/**
 * @file table.c
 * @brief Table storage implementation.
 * 
 * Tables are the data structure that store the component data. Tables have
 * columns for each component in the table, and rows for each entity stored in
 * the table. Once created, the component list for a table doesn't change, but
 * entities can move from one table to another.
 * 
 * Each table has a type, which is a vector with the (component) ids in the 
 * table. The vector is sorted by id, which ensures that there can be only one
 * table for each unique combination of components.
 * 
 * Not all ids in a table have to be components. Tags are ids that have no
 * data type associated with them, and as a result don't need to be explicitly
 * stored beyond an element in the table type. To save space and speed up table
 * creation, each table has a reference to a "storage table", which is a table
 * that only includes component ids (so excluding tags).
 * 
 * Note that the actual data is not stored on the storage table. The storage 
 * table is only used for sharing administration. A storage_map member maps
 * between column indices of the table and its storage table. Tables are 
 * refcounted, which ensures that storage tables won't be deleted if other
 * tables have references to it.
 */

#include "flecs.h"
/**
 * @file private_api.h
 * @brief Private functions.
 */

#ifndef FLECS_PRIVATE_H
#define FLECS_PRIVATE_H

/**
 * @file private_types.h
 * @brief Private types.
 */

#ifndef FLECS_PRIVATE_TYPES_H
#define FLECS_PRIVATE_TYPES_H

#ifndef __MACH__
#ifndef _POSIX_C_SOURCE
#define _POSIX_C_SOURCE 200809L
#endif
#endif

#include <stdlib.h>
#include <limits.h>
#include <stdio.h>

/**
 * @file datastructures/entity_index.h
 * @brief Entity index data structure.
 *
 * The entity index stores the table, row for an entity id.
 */
 
#ifndef FLECS_ENTITY_INDEX_H
#define FLECS_ENTITY_INDEX_H

#define FLECS_ENTITY_PAGE_SIZE (1 << FLECS_ENTITY_PAGE_BITS)
#define FLECS_ENTITY_PAGE_MASK (FLECS_ENTITY_PAGE_SIZE - 1)

typedef struct ecs_entity_index_page_t {
    ecs_record_t records[FLECS_ENTITY_PAGE_SIZE];
} ecs_entity_index_page_t;

typedef struct ecs_entity_index_t {
    ecs_vec_t dense;
    ecs_vec_t pages;
    int32_t alive_count;
    uint64_t max_id;
    ecs_block_allocator_t page_allocator;
    ecs_allocator_t *allocator;
} ecs_entity_index_t;

/** Initialize entity index. */
void flecs_entity_index_init(
    ecs_allocator_t *allocator,
    ecs_entity_index_t *index);

/** Deinitialize entity index. */
void flecs_entity_index_fini(
    ecs_entity_index_t *index);

/* Get entity (must exist/must be alive) */
ecs_record_t* flecs_entity_index_get(
    const ecs_entity_index_t *index,
    uint64_t entity);

/* Get entity (must exist/may not be alive) */
ecs_record_t* flecs_entity_index_get_any(
    const ecs_entity_index_t *index,
    uint64_t entity);

/* Get entity (may not exist/must be alive) */
ecs_record_t* flecs_entity_index_try_get(
    const ecs_entity_index_t *index,
    uint64_t entity);

/* Get entity (may not exist/may not be alive) */
ecs_record_t* flecs_entity_index_try_get_any(
    const ecs_entity_index_t *index,
    uint64_t entity);

/** Ensure entity exists. */
ecs_record_t* flecs_entity_index_ensure(
    ecs_entity_index_t *index,
    uint64_t entity);

/* Remove entity */
void flecs_entity_index_remove(
    ecs_entity_index_t *index,
    uint64_t entity);

/* Set generation of entity */
void flecs_entity_index_set_generation(
    ecs_entity_index_t *index,
    uint64_t entity);

/* Get current generation of entity */
uint64_t flecs_entity_index_get_generation(
    const ecs_entity_index_t *index,
    uint64_t entity);

/* Return whether entity is alive */
bool flecs_entity_index_is_alive(
    const ecs_entity_index_t *index,
    uint64_t entity);

/* Return whether entity is valid */
bool flecs_entity_index_is_valid(
    const ecs_entity_index_t *index,
    uint64_t entity);

/* Return whether entity exists */
bool flecs_entity_index_exists(
    const ecs_entity_index_t *index,
    uint64_t entity);

/* Create or recycle entity id */
uint64_t flecs_entity_index_new_id(
    ecs_entity_index_t *index);

/* Bulk create or recycle new entity ids */
uint64_t* flecs_entity_index_new_ids(
    ecs_entity_index_t *index,
    int32_t count);

/* Set size of index */
void flecs_entity_index_set_size(
    ecs_entity_index_t *index,
    int32_t size);

/* Return number of entities in index */
int32_t flecs_entity_index_count(
    const ecs_entity_index_t *index);

/* Return number of allocated entities in index */
int32_t flecs_entity_index_size(
    const ecs_entity_index_t *index);

/* Return number of not alive entities in index */
int32_t flecs_entity_index_not_alive_count(
    const ecs_entity_index_t *index);

/* Clear entity index */
void flecs_entity_index_clear(
    ecs_entity_index_t *index);

/* Return number of alive entities in index */
const uint64_t* flecs_entity_index_ids(
    const ecs_entity_index_t *index);

void flecs_entity_index_copy(
    ecs_entity_index_t *dst,
    const ecs_entity_index_t *src);

void flecs_entity_index_restore(
    ecs_entity_index_t *dst,
    const ecs_entity_index_t *src);

#define ecs_eis(world) (&((world)->store.entity_index))
#define flecs_entities_init(world) flecs_entity_index_init(&world->allocator, ecs_eis(world))
#define flecs_entities_fini(world) flecs_entity_index_fini(ecs_eis(world))
#define flecs_entities_get(world, entity) flecs_entity_index_get(ecs_eis(world), entity)
#define flecs_entities_try(world, entity) flecs_entity_index_try_get(ecs_eis(world), entity)
#define flecs_entities_get_any(world, entity) flecs_entity_index_get_any(ecs_eis(world), entity)
#define flecs_entities_ensure(world, entity) flecs_entity_index_ensure(ecs_eis(world), entity)
#define flecs_entities_remove(world, entity) flecs_entity_index_remove(ecs_eis(world), entity)
#define flecs_entities_set_generation(world, entity) flecs_entity_index_set_generation(ecs_eis(world), entity)
#define flecs_entities_get_generation(world, entity) flecs_entity_index_get_generation(ecs_eis(world), entity)
#define flecs_entities_is_alive(world, entity) flecs_entity_index_is_alive(ecs_eis(world), entity)
#define flecs_entities_is_valid(world, entity) flecs_entity_index_is_valid(ecs_eis(world), entity)
#define flecs_entities_exists(world, entity) flecs_entity_index_exists(ecs_eis(world), entity)
#define flecs_entities_new_id(world) flecs_entity_index_new_id(ecs_eis(world))
#define flecs_entities_new_ids(world, count) flecs_entity_index_new_ids(ecs_eis(world), count)
#define flecs_entities_max_id(world) (ecs_eis(world)->max_id)
#define flecs_entities_set_size(world, size) flecs_entity_index_set_size(ecs_eis(world), size)
#define flecs_entities_count(world) flecs_entity_index_count(ecs_eis(world))
#define flecs_entities_size(world) flecs_entity_index_size(ecs_eis(world))
#define flecs_entities_not_alive_count(world) flecs_entity_index_not_alive_count(ecs_eis(world))
#define flecs_entities_clear(world) flecs_entity_index_clear(ecs_eis(world))
#define flecs_entities_ids(world) flecs_entity_index_ids(ecs_eis(world))
#define flecs_entities_copy(dst, src) flecs_entity_index_copy(dst, src)
#define flecs_entities_restore(dst, src) flecs_entity_index_restore(dst, src)

#endif

/**
 * @file datastructures/stack_allocator.h
 * @brief Stack allocator.
 */

#ifndef FLECS_STACK_ALLOCATOR_H
#define FLECS_STACK_ALLOCATOR_H

/** Stack allocator for quick allocation of small temporary values */
#define ECS_STACK_PAGE_SIZE (4096)

typedef struct ecs_stack_page_t {
    void *data;
    struct ecs_stack_page_t *next;
    int16_t sp;
    uint32_t id;
} ecs_stack_page_t;

typedef struct ecs_stack_t {
    ecs_stack_page_t first;
    ecs_stack_page_t *cur;
} ecs_stack_t;

void flecs_stack_init(
    ecs_stack_t *stack);

void flecs_stack_fini(
    ecs_stack_t *stack);

void* flecs_stack_alloc(
    ecs_stack_t *stack, 
    ecs_size_t size,
    ecs_size_t align);

#define flecs_stack_alloc_t(stack, T)\
    flecs_stack_alloc(stack, ECS_SIZEOF(T), ECS_ALIGNOF(T))

#define flecs_stack_alloc_n(stack, T, count)\
    flecs_stack_alloc(stack, ECS_SIZEOF(T) * count, ECS_ALIGNOF(T))

void* flecs_stack_calloc(
    ecs_stack_t *stack, 
    ecs_size_t size,
    ecs_size_t align);

#define flecs_stack_calloc_t(stack, T)\
    flecs_stack_calloc(stack, ECS_SIZEOF(T), ECS_ALIGNOF(T))

#define flecs_stack_calloc_n(stack, T, count)\
    flecs_stack_calloc(stack, ECS_SIZEOF(T) * count, ECS_ALIGNOF(T))

void flecs_stack_free(
    void *ptr,
    ecs_size_t size);

#define flecs_stack_free_t(ptr, T)\
    flecs_stack_free(ptr, ECS_SIZEOF(T))

#define flecs_stack_free_n(ptr, T, count)\
    flecs_stack_free(ptr, ECS_SIZEOF(T) * count)

void flecs_stack_reset(
    ecs_stack_t *stack);

ecs_stack_cursor_t flecs_stack_get_cursor(
    ecs_stack_t *stack);

void flecs_stack_restore_cursor(
    ecs_stack_t *stack,
    const ecs_stack_cursor_t *cursor);

#endif

/**
 * @file bitset.h
 * @brief Bitset data structure.
 */

#ifndef FLECS_BITSET_H
#define FLECS_BITSET_H


#ifdef __cplusplus
extern "C" {
#endif

typedef struct ecs_bitset_t {
    uint64_t *data;
    int32_t count;
    ecs_size_t size;
} ecs_bitset_t;

/** Initialize bitset. */
FLECS_DBG_API
void flecs_bitset_init(
    ecs_bitset_t *bs);

/** Deinialize bitset. */
FLECS_DBG_API
void flecs_bitset_fini(
    ecs_bitset_t *bs);

/** Add n elements to bitset. */
FLECS_DBG_API
void flecs_bitset_addn(
    ecs_bitset_t *bs,
    int32_t count);

/** Ensure element exists. */
FLECS_DBG_API
void flecs_bitset_ensure(
    ecs_bitset_t *bs,
    int32_t count);

/** Set element. */
FLECS_DBG_API
void flecs_bitset_set(
    ecs_bitset_t *bs,
    int32_t elem,
    bool value);

/** Get element. */
FLECS_DBG_API
bool flecs_bitset_get(
    const ecs_bitset_t *bs,
    int32_t elem);

/** Return number of elements. */
FLECS_DBG_API
int32_t flecs_bitset_count(
    const ecs_bitset_t *bs);

/** Remove from bitset. */
FLECS_DBG_API
void flecs_bitset_remove(
    ecs_bitset_t *bs,
    int32_t elem);

/** Swap values in bitset. */
FLECS_DBG_API
void flecs_bitset_swap(
    ecs_bitset_t *bs,
    int32_t elem_a,
    int32_t elem_b);

#ifdef __cplusplus
}
#endif

#endif

/**
 * @file switch_list.h
 * @brief Interleaved linked list for storing mutually exclusive values.
 */

#ifndef FLECS_SWITCH_LIST_H
#define FLECS_SWITCH_LIST_H


typedef struct ecs_switch_header_t {
    int32_t element;    /* First element for value */
    int32_t count;      /* Number of elements for value */
} ecs_switch_header_t;

typedef struct ecs_switch_node_t {
    int32_t next;       /* Next node in list */
    int32_t prev;       /* Prev node in list */
} ecs_switch_node_t;

struct ecs_switch_t {    
    ecs_map_t hdrs;     /* map<uint64_t, ecs_switch_header_t> */
    ecs_vec_t nodes;    /* vec<ecs_switch_node_t> */
    ecs_vec_t values;   /* vec<uint64_t> */
};

/** Init new switch. */
FLECS_DBG_API
void flecs_switch_init(
    ecs_switch_t* sw,
    ecs_allocator_t *allocator,
    int32_t elements);

/** Fini switch. */
FLECS_DBG_API
void flecs_switch_fini(
    ecs_switch_t *sw);

/** Remove all values. */
FLECS_DBG_API
void flecs_switch_clear(
    ecs_switch_t *sw);

/** Add element to switch, initialize value to 0 */
FLECS_DBG_API
void flecs_switch_add(
    ecs_switch_t *sw);

/** Set number of elements in switch list */
FLECS_DBG_API
void flecs_switch_set_count(
    ecs_switch_t *sw,
    int32_t count);

/** Get number of elements */
FLECS_DBG_API
int32_t flecs_switch_count(
    ecs_switch_t *sw);

/** Ensure that element exists. */
FLECS_DBG_API
void flecs_switch_ensure(
    ecs_switch_t *sw,
    int32_t count);

/** Add n elements. */
FLECS_DBG_API
void flecs_switch_addn(
    ecs_switch_t *sw,
    int32_t count);    

/** Set value of element. */
FLECS_DBG_API
void flecs_switch_set(
    ecs_switch_t *sw,
    int32_t element,
    uint64_t value);

/** Remove element. */
FLECS_DBG_API
void flecs_switch_remove(
    ecs_switch_t *sw,
    int32_t element);

/** Get value for element. */
FLECS_DBG_API
uint64_t flecs_switch_get(
    const ecs_switch_t *sw,
    int32_t element);

/** Swap element. */
FLECS_DBG_API
void flecs_switch_swap(
    ecs_switch_t *sw,
    int32_t elem_1,
    int32_t elem_2);

/** Get vector with all values. Use together with count(). */
FLECS_DBG_API
ecs_vec_t* flecs_switch_values(
    const ecs_switch_t *sw);    

/** Return number of different values. */
FLECS_DBG_API
int32_t flecs_switch_case_count(
    const ecs_switch_t *sw,
    uint64_t value);

/** Return first element for value. */
FLECS_DBG_API
int32_t flecs_switch_first(
    const ecs_switch_t *sw,
    uint64_t value);

/** Return next element for value. Use with first(). */
FLECS_DBG_API
int32_t flecs_switch_next(
    const ecs_switch_t *sw,
    int32_t elem);

#ifdef __cplusplus
extern "C" {
#endif

#ifdef __cplusplus
}
#endif

#endif


/* Used in id records to keep track of entities used with id flags */
extern const ecs_entity_t EcsFlag;

#define ECS_MAX_JOBS_PER_WORKER (16)

/* Magic number for a flecs object */
#define ECS_OBJECT_MAGIC (0x6563736f)

/* Tags associated with poly for (Poly, tag) components */
#define ecs_world_t_tag     invalid
#define ecs_stage_t_tag     invalid
#define ecs_query_t_tag     EcsQuery
#define ecs_rule_t_tag      EcsQuery
#define ecs_table_t_tag     invalid
#define ecs_filter_t_tag    EcsQuery
#define ecs_observer_t_tag  EcsObserver

/* Mixin kinds */
typedef enum ecs_mixin_kind_t {
    EcsMixinWorld,
    EcsMixinEntity,
    EcsMixinObservable,
    EcsMixinIterable,
    EcsMixinDtor,
    EcsMixinMax
} ecs_mixin_kind_t;

/* The mixin array contains pointers to mixin members for different kinds of
 * flecs objects. This allows the API to retrieve data from an object regardless
 * of its type. Each mixin array is only stored once per type */
struct ecs_mixins_t {
    const char *type_name; /* Include name of mixin type so debug code doesn't
                            * need to know about every object */
    ecs_size_t elems[EcsMixinMax];                        
};

/* Mixin tables */
extern ecs_mixins_t ecs_world_t_mixins;
extern ecs_mixins_t ecs_stage_t_mixins;
extern ecs_mixins_t ecs_filter_t_mixins;
extern ecs_mixins_t ecs_query_t_mixins;
extern ecs_mixins_t ecs_trigger_t_mixins;
extern ecs_mixins_t ecs_observer_t_mixins;

/* Types that have no mixins */
#define ecs_table_t_mixins (&(ecs_mixins_t){ NULL })

/* Scope for flecs internals, like observers used for builtin features */
extern const ecs_entity_t EcsFlecsInternals;

/** Type used for internal string hashmap */
typedef struct ecs_hashed_string_t {
    char *value;
    ecs_size_t length;
    uint64_t hash;
} ecs_hashed_string_t;

/* Table event type for notifying tables of world events */
typedef enum ecs_table_eventkind_t {
    EcsTableTriggersForId,
    EcsTableNoTriggersForId,
} ecs_table_eventkind_t;

typedef struct ecs_table_event_t {
    ecs_table_eventkind_t kind;

    /* Query event */
    ecs_query_t *query;

    /* Component info event */
    ecs_entity_t component;

    /* Event match */
    ecs_entity_t event;

    /* If the nubmer of fields gets out of hand, this can be turned into a union
     * but since events are very temporary objects, this works for now and makes
     * initializing an event a bit simpler. */
} ecs_table_event_t;

/** Stage-specific component data */
struct ecs_data_t {
    ecs_vec_t entities;              /* Entity identifiers */
    ecs_vec_t records;               /* Ptrs to records in main entity index */
    ecs_vec_t *columns;              /* Component columns */
};

/** Cache of added/removed components for non-trivial edges between tables */
#define ECS_TABLE_DIFF_INIT { .added = {0}}

typedef struct ecs_table_diff_t {
    ecs_type_t added;                /* Components added between tables */
    ecs_type_t removed;              /* Components removed between tables */
} ecs_table_diff_t;

/** Builder for table diff. The table diff type itself doesn't use ecs_vec_t to
 * conserve memory on table edges (a type doesn't have the size field), whereas
 * a vec for the builder is more convenient to use & has allocator support. */
typedef struct ecs_table_diff_builder_t {
    ecs_vec_t added;
    ecs_vec_t removed;
} ecs_table_diff_builder_t;

/** Edge linked list (used to keep track of incoming edges) */
typedef struct ecs_graph_edge_hdr_t {
    struct ecs_graph_edge_hdr_t *prev;
    struct ecs_graph_edge_hdr_t *next;
} ecs_graph_edge_hdr_t;

/** Single edge. */
typedef struct ecs_graph_edge_t {
    ecs_graph_edge_hdr_t hdr;
    ecs_table_t *from;               /* Edge source table */
    ecs_table_t *to;                 /* Edge destination table */
    ecs_table_diff_t *diff;          /* Index into diff vector, if non trivial edge */
    ecs_id_t id;                     /* Id associated with edge */
} ecs_graph_edge_t;

/* Edges to other tables. */
typedef struct ecs_graph_edges_t {
    ecs_graph_edge_t *lo;            /* Small array optimized for low edges */
    ecs_map_t *hi;                   /* Map for hi edges (map<id, edge_t>) */
} ecs_graph_edges_t;

/* Table graph node */
typedef struct ecs_graph_node_t {
    /* Outgoing edges */
    ecs_graph_edges_t add;    
    ecs_graph_edges_t remove; 

    /* Incoming edges (next = add edges, prev = remove edges) */
    ecs_graph_edge_hdr_t refs;
} ecs_graph_node_t;

/** Infrequently accessed data not stored inline in ecs_table_t */
typedef struct ecs_table__t {
    uint64_t hash;                   /* Type hash */
    int32_t lock;                    /* Prevents modifications */
    int32_t refcount;                /* Increased when used as storage table */
    int32_t traversable_count;       /* Number of observed entities in table */
    uint16_t generation;             /* Used for table cleanup */
    uint16_t record_count;           /* Table record count including wildcards */
    
    struct ecs_table_record_t *records; /* Array with table records */
    ecs_hashmap_t *name_index;       /* Cached pointer to name index */

    ecs_switch_t *sw_columns;        /* Switch columns */
    ecs_bitset_t *bs_columns;        /* Bitset columns */
    int16_t sw_count;
    int16_t sw_offset;
    int16_t bs_count;
    int16_t bs_offset;
    int16_t ft_offset;
} ecs_table__t;

/** A table is the Flecs equivalent of an archetype. Tables store all entities
 * with a specific set of components. Tables are automatically created when an
 * entity has a set of components not previously observed before. When a new
 * table is created, it is automatically matched with existing queries */
struct ecs_table_t {
    uint64_t id;                     /* Table id in sparse set */
    ecs_flags32_t flags;             /* Flags for testing table properties */
    uint16_t storage_count;          /* Number of components (excluding tags) */
    ecs_type_t type;                 /* Identifies table type in type_index */

    ecs_graph_node_t node;           /* Graph node */
    ecs_data_t data;                 /* Component storage */
    ecs_type_info_t **type_info;     /* Cached type info */
    int32_t *dirty_state;            /* Keep track of changes in columns */

    ecs_table_t *storage_table;      /* Table without tags */
    ecs_id_t *storage_ids;           /* Component ids (prevent indirection) */
    int32_t *storage_map;            /* Map type <-> data type
                                      *  - 0..count(T):        type -> data_type
                                      *  - count(T)..count(S): data_type -> type
                                      */

    ecs_table__t *_;                 /* Infrequently accessed table metadata */
};

/** Must appear as first member in payload of table cache */
typedef struct ecs_table_cache_hdr_t {
    struct ecs_table_cache_t *cache;
    ecs_table_t *table;
    struct ecs_table_cache_hdr_t *prev, *next;
    bool empty;
} ecs_table_cache_hdr_t;

/** Linked list of tables in table cache */
typedef struct ecs_table_cache_list_t {
    ecs_table_cache_hdr_t *first;
    ecs_table_cache_hdr_t *last;
    int32_t count;
} ecs_table_cache_list_t;

/** Table cache */
typedef struct ecs_table_cache_t {
    ecs_map_t index; /* <table_id, T*> */
    ecs_table_cache_list_t tables;
    ecs_table_cache_list_t empty_tables;
} ecs_table_cache_t;

/* Sparse query term */
typedef struct flecs_switch_term_t {
    ecs_switch_t *sw_column;
    ecs_entity_t sw_case; 
    int32_t signature_column_index;
} flecs_switch_term_t;

/* Bitset query term */
typedef struct flecs_bitset_term_t {
    ecs_bitset_t *bs_column;
    int32_t column_index;
} flecs_bitset_term_t;

typedef struct flecs_flat_monitor_t {
    int32_t table_state;
    int32_t monitor;
} flecs_flat_monitor_t;

/* Flat table term */
typedef struct flecs_flat_table_term_t {
    int32_t field_index; /* Iterator field index */
    ecs_term_t *term;
    ecs_vec_t monitor;
} flecs_flat_table_term_t;

/* Entity filter. This filters the entities of a matched table, for example when
 * it has disabled components or union relationships (switch). */
typedef struct ecs_entity_filter_t {
    ecs_vec_t sw_terms;              /* Terms with switch (union) entity filter */
    ecs_vec_t bs_terms;              /* Terms with bitset (toggle) entity filter */
    ecs_vec_t ft_terms;              /* Terms with components from flattened tree */
    int32_t flat_tree_column;
} ecs_entity_filter_t;

typedef struct ecs_entity_filter_iter_t {
    ecs_entity_filter_t *entity_filter;
    ecs_iter_t *it;
    int32_t *columns;
    ecs_table_t *prev;
    ecs_table_range_t range;
    int32_t bs_offset;
    int32_t sw_offset;
    int32_t sw_smallest;
    int32_t flat_tree_offset;
    int32_t target_count;
} ecs_entity_filter_iter_t;

/** Table match data.
 * Each table matched by the query is represented by a ecs_query_table_match_t
 * instance, which are linked together in a list. A table may match a query
 * multiple times (due to wildcard queries) with different columns being matched
 * by the query. */
struct ecs_query_table_match_t {
    ecs_query_table_match_t *next, *prev;
    ecs_table_t *table;              /* The current table. */
    int32_t offset;                  /* Starting point in table  */
    int32_t count;                   /* Number of entities to iterate in table */
    int32_t *columns;                /* Mapping from query fields to table columns */
    int32_t *storage_columns;        /* Mapping from query fields to storage columns */
    ecs_id_t *ids;                   /* Resolved (component) ids for current table */
    ecs_entity_t *sources;           /* Subjects (sources) of ids */
    ecs_vec_t refs;                  /* Cached components for non-this terms */
    uint64_t group_id;               /* Value used to organize tables in groups */
    int32_t *monitor;                /* Used to monitor table for changes */
    ecs_entity_filter_t *entity_filter; /* Entity specific filters */

    /* Next match in cache for same table (includes empty tables) */
    ecs_query_table_match_t *next_match;
};

/** Table record type for query table cache. A query only has one per table. */
typedef struct ecs_query_table_t {
    ecs_table_cache_hdr_t hdr;       /* Header for ecs_table_cache_t */
    ecs_query_table_match_t *first;  /* List with matches for table */
    ecs_query_table_match_t *last;   /* Last discovered match for table */
    uint64_t table_id;
    int32_t rematch_count;           /* Track whether table was rematched */
} ecs_query_table_t;

/** Points to the beginning & ending of a query group */
typedef struct ecs_query_table_list_t {
    ecs_query_table_match_t *first;
    ecs_query_table_match_t *last;
    ecs_query_group_info_t info;
} ecs_query_table_list_t;

/* Query event type for notifying queries of world events */
typedef enum ecs_query_eventkind_t {
    EcsQueryTableMatch,
    EcsQueryTableRematch,
    EcsQueryTableUnmatch,
    EcsQueryOrphan
} ecs_query_eventkind_t;

typedef struct ecs_query_event_t {
    ecs_query_eventkind_t kind;
    ecs_table_t *table;
    ecs_query_t *parent_query;
} ecs_query_event_t;

/* Query level block allocators have sizes that depend on query field count */
typedef struct ecs_query_allocators_t {
    ecs_block_allocator_t columns;
    ecs_block_allocator_t ids;
    ecs_block_allocator_t sources;
    ecs_block_allocator_t monitors;
} ecs_query_allocators_t;

/** Query that is automatically matched against tables */
struct ecs_query_t {
    ecs_header_t hdr;

    /* Query filter */
    ecs_filter_t filter;

    /* Tables matched with query */
    ecs_table_cache_t cache;

    /* Linked list with all matched non-empty tables, in iteration order */
    ecs_query_table_list_t list;

    /* Contains head/tail to nodes of query groups (if group_by is used) */
    ecs_map_t groups;

    /* Table sorting */
    ecs_entity_t order_by_component;
    ecs_order_by_action_t order_by;
    ecs_sort_table_action_t sort_table;
    ecs_vec_t table_slices;
    int32_t order_by_term;

    /* Table grouping */
    ecs_entity_t group_by_id;
    ecs_group_by_action_t group_by;
    ecs_group_create_action_t on_group_create;
    ecs_group_delete_action_t on_group_delete;
    void *group_by_ctx;
    ecs_ctx_free_t group_by_ctx_free;

    /* Subqueries */
    ecs_query_t *parent;
    ecs_vec_t subqueries;

    /* Flags for query properties */
    ecs_flags32_t flags;

    /* Monitor generation */
    int32_t monitor_generation;

    int32_t cascade_by;              /* Identify cascade column */
    int32_t match_count;             /* How often have tables been (un)matched */
    int32_t prev_match_count;        /* Track if sorting is needed */
    int32_t rematch_count;           /* Track which tables were added during rematch */

    /* Mixins */
    ecs_iterable_t iterable;
    ecs_poly_dtor_t dtor;

    /* Query-level allocators */
    ecs_query_allocators_t allocators;
};

/** All observers for a specific (component) id */
typedef struct ecs_event_id_record_t {
    /* Triggers for Self */
    ecs_map_t self;                  /* map<trigger_id, trigger_t> */
    ecs_map_t self_up;               /* map<trigger_id, trigger_t> */
    ecs_map_t up;                    /* map<trigger_id, trigger_t> */

    ecs_map_t observers;             /* map<trigger_id, trigger_t> */

    /* Triggers for SuperSet, SubSet */
    ecs_map_t set_observers;         /* map<trigger_id, trigger_t> */

    /* Triggers for Self with non-This subject */
    ecs_map_t entity_observers;      /* map<trigger_id, trigger_t> */

    /* Number of active observers for (component) id */
    int32_t observer_count;
} ecs_event_id_record_t;

/* World level allocators are for operations that are not multithreaded */
typedef struct ecs_world_allocators_t {
    ecs_map_params_t ptr;
    ecs_map_params_t query_table_list;
    ecs_block_allocator_t query_table;
    ecs_block_allocator_t query_table_match;
    ecs_block_allocator_t graph_edge_lo;
    ecs_block_allocator_t graph_edge;
    ecs_block_allocator_t id_record;
    ecs_block_allocator_t id_record_chunk;
    ecs_block_allocator_t table_diff;
    ecs_block_allocator_t sparse_chunk;
    ecs_block_allocator_t hashmap;

    /* Temporary vectors used for creating table diff id sequences */
    ecs_table_diff_builder_t diff_builder;
} ecs_world_allocators_t;

/* Stage level allocators are for operations that can be multithreaded */
typedef struct ecs_stage_allocators_t {
    ecs_stack_t iter_stack;
    ecs_stack_t deser_stack;
    ecs_block_allocator_t cmd_entry_chunk;
} ecs_stage_allocators_t;

/** Types for deferred operations */
typedef enum ecs_cmd_kind_t {
    EcsOpClone,
    EcsOpBulkNew,
    EcsOpAdd,
    EcsOpRemove,   
    EcsOpSet,
    EcsOpEmplace,
    EcsOpMut,
    EcsOpModified,
    EcsOpAddModified,
    EcsOpPath,
    EcsOpDelete,
    EcsOpClear,
    EcsOpOnDeleteAction,
    EcsOpEnable,
    EcsOpDisable,
    EcsOpSkip
} ecs_cmd_kind_t;

typedef struct ecs_cmd_1_t {
    void *value;                     /* Component value (used by set / get_mut) */
    ecs_size_t size;                 /* Size of value */
    bool clone_value;                /* Clone entity with value (used for clone) */ 
} ecs_cmd_1_t;

typedef struct ecs_cmd_n_t {
    ecs_entity_t *entities;  
    int32_t count;
} ecs_cmd_n_t;

typedef struct ecs_cmd_t {
    ecs_cmd_kind_t kind;             /* Command kind */
    int32_t next_for_entity;         /* Next operation for entity */    
    ecs_id_t id;                     /* (Component) id */
    ecs_id_record_t *idr;            /* Id record (only for set/mut/emplace) */
    ecs_entity_t entity;             /* Entity id */

    union {
        ecs_cmd_1_t _1;              /* Data for single entity operation */
        ecs_cmd_n_t _n;              /* Data for multi entity operation */
    } is;
} ecs_cmd_t;

/* Entity specific metadata for command in defer queue */
typedef struct ecs_cmd_entry_t {
    int32_t first;
    int32_t last;                    /* If -1, a delete command was inserted */
} ecs_cmd_entry_t;

/** A stage is a context that allows for safely using the API from multiple 
 * threads. Stage pointers can be passed to the world argument of API 
 * operations, which causes the operation to be ran on the stage instead of the
 * world. */
struct ecs_stage_t {
    ecs_header_t hdr;

    /* Unique id that identifies the stage */
    int32_t id;

    /* Deferred command queue */
    int32_t defer;
    ecs_vec_t commands;
    ecs_stack_t defer_stack;         /* Temp memory used by deferred commands */
    ecs_sparse_t cmd_entries;        /* <entity, op_entry_t> - command combining */

    /* Thread context */
    ecs_world_t *thread_ctx;         /* Points to stage when a thread stage */
    ecs_world_t *world;              /* Reference to world */
    ecs_os_thread_t thread;          /* Thread handle (0 if no threading is used) */

    /* One-shot actions to be executed after the merge */
    ecs_vec_t post_frame_actions;

    /* Namespacing */
    ecs_entity_t scope;              /* Entity of current scope */
    ecs_entity_t with;               /* Id to add by default to new entities */
    ecs_entity_t base;               /* Currently instantiated top-level base */
    ecs_entity_t *lookup_path;       /* Search path used by lookup operations */

    /* Properties */
    bool auto_merge;                 /* Should this stage automatically merge? */
    bool async;                      /* Is stage asynchronous? (write only) */

    /* Thread specific allocators */
    ecs_stage_allocators_t allocators;
    ecs_allocator_t allocator;

    /* Caches for rule creation */
    ecs_vec_t variables;
    ecs_vec_t operations;
};

/* Component monitor */
typedef struct ecs_monitor_t {
    ecs_vec_t queries;               /* vector<ecs_query_t*> */
    bool is_dirty;                   /* Should queries be rematched? */
} ecs_monitor_t;

/* Component monitors */
typedef struct ecs_monitor_set_t {
    ecs_map_t monitors;              /* map<id, ecs_monitor_t> */
    bool is_dirty;                   /* Should monitors be evaluated? */
} ecs_monitor_set_t;

/* Data stored for id marked for deletion */
typedef struct ecs_marked_id_t {
    ecs_id_record_t *idr;
    ecs_id_t id;
    ecs_entity_t action;             /* Set explicitly for delete_with, remove_all */
    bool delete_id;
} ecs_marked_id_t;

typedef struct ecs_store_t {
    /* Entity lookup */
    ecs_entity_index_t entity_index;

    /* Table lookup by id */
    ecs_sparse_t tables;             /* sparse<table_id, ecs_table_t> */

    /* Table lookup by hash */
    ecs_hashmap_t table_map;         /* hashmap<ecs_type_t, ecs_table_t*> */

    /* Root table */
    ecs_table_t root;

    /* Records cache */
    ecs_vec_t records;

    /* Stack of ids being deleted. */
    ecs_vec_t marked_ids;            /* vector<ecs_marked_ids_t> */
    
    /* Entity ids associated with depth (for flat hierarchies) */
    ecs_vec_t depth_ids;
    ecs_map_t entity_to_depth; /* What it says */
} ecs_store_t;

/* fini actions */
typedef struct ecs_action_elem_t {
    ecs_fini_action_t action;
    void *ctx;
} ecs_action_elem_t;

/** The world stores and manages all ECS data. An application can have more than
 * one world, but data is not shared between worlds. */
struct ecs_world_t {
    ecs_header_t hdr;

    /* --  Type metadata -- */
    ecs_id_record_t *id_index_lo;
    ecs_map_t id_index_hi;           /* map<id, ecs_id_record_t*> */
    ecs_sparse_t type_info;          /* sparse<type_id, type_info_t> */

    /* -- Cached handle to id records -- */
    ecs_id_record_t *idr_wildcard;
    ecs_id_record_t *idr_wildcard_wildcard;
    ecs_id_record_t *idr_any;
    ecs_id_record_t *idr_isa_wildcard;
    ecs_id_record_t *idr_childof_0;
    ecs_id_record_t *idr_childof_wildcard;
    ecs_id_record_t *idr_identifier_name;

    /* -- Mixins -- */
    ecs_world_t *self;
    ecs_observable_t observable;
    ecs_iterable_t iterable;

    /* Unique id per generated event used to prevent duplicate notifications */
    int32_t event_id;

    /* Is entity range checking enabled? */
    bool range_check_enabled;

    /* --  Data storage -- */
    ecs_store_t store;

    /* --  Pending table event buffers -- */
    ecs_sparse_t *pending_buffer;    /* sparse<table_id, ecs_table_t*> */
    ecs_sparse_t *pending_tables;    /* sparse<table_id, ecs_table_t*> */

    /* Used to track when cache needs to be updated */
    ecs_monitor_set_t monitors;      /* map<id, ecs_monitor_t> */

    /* -- Systems -- */
    ecs_entity_t pipeline;           /* Current pipeline */

    /* -- Identifiers -- */
    ecs_hashmap_t aliases;
    ecs_hashmap_t symbols;

    /* -- Staging -- */
    ecs_stage_t *stages;             /* Stages */
    int32_t stage_count;             /* Number of stages */

    /* -- Multithreading -- */
    ecs_os_cond_t worker_cond;       /* Signal that worker threads can start */
    ecs_os_cond_t sync_cond;         /* Signal that worker thread job is done */
    ecs_os_mutex_t sync_mutex;       /* Mutex for job_cond */
    int32_t workers_running;         /* Number of threads running */
    int32_t workers_waiting;         /* Number of workers waiting on sync */
    bool workers_use_task_api;   /* Workers are short-lived tasks, not long-running threads */

    /* -- Time management -- */
    ecs_time_t world_start_time;     /* Timestamp of simulation start */
    ecs_time_t frame_start_time;     /* Timestamp of frame start */
    ecs_ftime_t fps_sleep;           /* Sleep time to prevent fps overshoot */

    /* -- Metrics -- */
    ecs_world_info_t info;

    /* -- World flags -- */
    ecs_flags32_t flags;

    /* Count that increases when component monitors change */
    int32_t monitor_generation;

    /* -- Allocators -- */
    ecs_world_allocators_t allocators; /* Static allocation sizes */
    ecs_allocator_t allocator;       /* Dynamic allocation sizes */

    void *context;                   /* Application context */
    ecs_vec_t fini_actions;          /* Callbacks to execute when world exits */
};

#endif

/**
 * @file table_cache.h
 * @brief Data structure for fast table iteration/lookups.
 */

#ifndef FLECS_TABLE_CACHE_H_
#define FLECS_TABLE_CACHE_H_

void ecs_table_cache_init(
    ecs_world_t *world,
    ecs_table_cache_t *cache);

void ecs_table_cache_fini(
    ecs_table_cache_t *cache);

void ecs_table_cache_insert(
    ecs_table_cache_t *cache,
    const ecs_table_t *table,
    ecs_table_cache_hdr_t *result);

void ecs_table_cache_replace(
    ecs_table_cache_t *cache,
    const ecs_table_t *table,
    ecs_table_cache_hdr_t *elem);

void* ecs_table_cache_remove(
    ecs_table_cache_t *cache,
    uint64_t table_id,
    ecs_table_cache_hdr_t *elem);

void* ecs_table_cache_get(
    const ecs_table_cache_t *cache,
    const ecs_table_t *table);

bool ecs_table_cache_set_empty(
    ecs_table_cache_t *cache,
    const ecs_table_t *table,
    bool empty);

bool ecs_table_cache_is_empty(
    const ecs_table_cache_t *cache);

#define flecs_table_cache_count(cache) (cache)->tables.count
#define flecs_table_cache_empty_count(cache) (cache)->empty_tables.count

bool flecs_table_cache_iter(
    ecs_table_cache_t *cache,
    ecs_table_cache_iter_t *out);

bool flecs_table_cache_empty_iter(
    ecs_table_cache_t *cache,
    ecs_table_cache_iter_t *out);

bool flecs_table_cache_all_iter(
    ecs_table_cache_t *cache,
    ecs_table_cache_iter_t *out);

ecs_table_cache_hdr_t* _flecs_table_cache_next(
    ecs_table_cache_iter_t *it);

#define flecs_table_cache_next(it, T)\
    (ECS_CAST(T*, _flecs_table_cache_next(it)))

#endif

/**
 * @file id_record.h
 * @brief Index for looking up tables by (component) id.
 */

#ifndef FLECS_ID_RECORD_H
#define FLECS_ID_RECORD_H

/* Payload for id cache */
struct ecs_table_record_t {
    ecs_table_cache_hdr_t hdr;  /* Table cache header */
    int32_t column;             /* First column where id occurs in table */
    int32_t count;              /* Number of times id occurs in table */
};

/* Linked list of id records */
typedef struct ecs_id_record_elem_t {
    struct ecs_id_record_t *prev, *next;
} ecs_id_record_elem_t;

typedef struct ecs_reachable_elem_t {
    const ecs_table_record_t *tr;
    ecs_record_t *record;
    ecs_entity_t src;
    ecs_id_t id;
#ifndef NDEBUG
    ecs_table_t *table;
#endif
} ecs_reachable_elem_t;

typedef struct ecs_reachable_cache_t {
    int32_t generation;
    int32_t current;
    ecs_vec_t ids; /* vec<reachable_elem_t> */
} ecs_reachable_cache_t;

/* Payload for id index which contains all datastructures for an id. */
struct ecs_id_record_t {
    /* Cache with all tables that contain the id. Must be first member. */
    ecs_table_cache_t cache; /* table_cache<ecs_table_record_t> */

    /* Id of record */
    ecs_id_t id;

    /* Flags for id */
    ecs_flags32_t flags;

    /* Cached pointer to type info for id, if id contains data. */
    const ecs_type_info_t *type_info;

    /* Name lookup index (currently only used for ChildOf pairs) */
    ecs_hashmap_t *name_index;

    /* Lists for all id records that match a pair wildcard. The wildcard id
     * record is at the head of the list. */
    ecs_id_record_elem_t first;   /* (R, *) */
    ecs_id_record_elem_t second;  /* (*, O) */
    ecs_id_record_elem_t trav;    /* (*, O) with only traversable relationships */

    /* Parent id record. For pair records the parent is the (R, *) record. */
    ecs_id_record_t *parent;

    /* Refcount */
    int32_t refcount;

    /* Keep alive count. This count must be 0 when the id record is deleted. If
     * it is not 0, an application attempted to delete an id that was still
     * queried for. */
    int32_t keep_alive;

    /* Cache invalidation counter */
    ecs_reachable_cache_t reachable;
};

/* Get id record for id */
ecs_id_record_t* flecs_id_record_get(
    const ecs_world_t *world,
    ecs_id_t id);

/* Get id record for id for searching.
 * Same as flecs_id_record_get, but replaces (R, *) with (Union, R) if R is a
 * union relationship. */
ecs_id_record_t* flecs_query_id_record_get(
    const ecs_world_t *world,
    ecs_id_t id);

/* Ensure id record for id */
ecs_id_record_t* flecs_id_record_ensure(
    ecs_world_t *world,
    ecs_id_t id);

/* Increase refcount of id record */
void flecs_id_record_claim(
    ecs_world_t *world,
    ecs_id_record_t *idr);

/* Decrease refcount of id record, delete if 0 */
int32_t flecs_id_record_release(
    ecs_world_t *world,
    ecs_id_record_t *idr);

/* Release all empty tables in id record */
void flecs_id_record_release_tables(
    ecs_world_t *world,
    ecs_id_record_t *idr);

/* Set (component) type info for id record */
bool flecs_id_record_set_type_info(
    ecs_world_t *world,
    ecs_id_record_t *idr,
    const ecs_type_info_t *ti);

/* Ensure id record has name index */
ecs_hashmap_t* flecs_id_name_index_ensure(
    ecs_world_t *world,
    ecs_id_t id);

ecs_hashmap_t* flecs_id_record_name_index_ensure(
    ecs_world_t *world,
    ecs_id_record_t *idr);

/* Get name index for id record */
ecs_hashmap_t* flecs_id_name_index_get(
    const ecs_world_t *world,
    ecs_id_t id);

/* Find table record for id */
ecs_table_record_t* flecs_table_record_get(
    const ecs_world_t *world,
    const ecs_table_t *table,
    ecs_id_t id);

/* Find table record for id record */
const ecs_table_record_t* flecs_id_record_get_table(
    const ecs_id_record_t *idr,
    const ecs_table_t *table);

/* Bootstrap cached id records */
void flecs_init_id_records(
    ecs_world_t *world);

/* Cleanup all id records in world */
void flecs_fini_id_records(
    ecs_world_t *world);

#endif

/**
 * @file observable.h
 * @brief Functions for sending events.
 */

#ifndef FLECS_OBSERVABLE_H
#define FLECS_OBSERVABLE_H

ecs_event_record_t* flecs_event_record_get(
    const ecs_observable_t *o,
    ecs_entity_t event);

ecs_event_record_t* flecs_event_record_ensure(
    ecs_observable_t *o,
    ecs_entity_t event);

ecs_event_id_record_t* flecs_event_id_record_get(
    const ecs_event_record_t *er,
    ecs_id_t id);

ecs_event_id_record_t* flecs_event_id_record_ensure(
    ecs_world_t *world,
    ecs_event_record_t *er,
    ecs_id_t id);

void flecs_event_id_record_remove(
    ecs_event_record_t *er,
    ecs_id_t id);

void flecs_observable_init(
    ecs_observable_t *observable);

void flecs_observable_fini(
    ecs_observable_t *observable);

bool flecs_observers_exist(
    ecs_observable_t *observable,
    ecs_id_t id,
    ecs_entity_t event);

void flecs_observer_fini(
    ecs_observer_t *observer);

void flecs_emit( 
    ecs_world_t *world,
    ecs_world_t *stage,
    ecs_event_desc_t *desc);

bool flecs_default_observer_next_callback(
    ecs_iter_t *it);

void flecs_observers_invoke(
    ecs_world_t *world,
    ecs_map_t *observers,
    ecs_iter_t *it,
    ecs_table_t *table,
    ecs_entity_t trav,
    int32_t evtx);

void flecs_emit_propagate_invalidate(
    ecs_world_t *world,
    ecs_table_t *table,
    int32_t offset,
    int32_t count);

#endif

/**
 * @file iter.h
 * @brief Iterator utilities.
 */

#ifndef FLECS_ITER_H
#define FLECS_ITER_H

void flecs_iter_init(
    const ecs_world_t *world,
    ecs_iter_t *it,
    ecs_flags8_t fields);

void flecs_iter_validate(
    ecs_iter_t *it);

void flecs_iter_populate_data(
    ecs_world_t *world,
    ecs_iter_t *it,
    ecs_table_t *table,
    int32_t offset,
    int32_t count,
    void **ptrs);

bool flecs_iter_next_row(
    ecs_iter_t *it);

bool flecs_iter_next_instanced(
    ecs_iter_t *it,
    bool result);

void* flecs_iter_calloc(
    ecs_iter_t *it,
    ecs_size_t size,
    ecs_size_t align);

#define flecs_iter_calloc_t(it, T)\
    flecs_iter_calloc(it, ECS_SIZEOF(T), ECS_ALIGNOF(T))

#define flecs_iter_calloc_n(it, T, count)\
    flecs_iter_calloc(it, ECS_SIZEOF(T) * count, ECS_ALIGNOF(T))

void flecs_iter_free(
    void *ptr,
    ecs_size_t size);

#define flecs_iter_free_t(ptr, T)\
    flecs_iter_free(ptr, ECS_SIZEOF(T))

#define flecs_iter_free_n(ptr, T, count)\
    flecs_iter_free(ptr, ECS_SIZEOF(T) * count)

#endif

/**
 * @file table.c
 * @brief Table storage implementation.
 */

#ifndef FLECS_TABLE_H
#define FLECS_TABLE_H

/* Init table */
void flecs_table_init(
    ecs_world_t *world,
    ecs_table_t *table,
    ecs_table_t *from);

/** Copy type. */
ecs_type_t flecs_type_copy(
    ecs_world_t *world,
    const ecs_type_t *src);

/** Free type. */
void flecs_type_free(
    ecs_world_t *world,
    ecs_type_t *type);

/** Find or create table for a set of components */
ecs_table_t* flecs_table_find_or_create(
    ecs_world_t *world,
    ecs_type_t *type);

/* Initialize columns for data */
void flecs_table_init_data(
    ecs_world_t *world,
    ecs_table_t *table); 

/* Clear all entities from a table. */
void flecs_table_clear_entities(
    ecs_world_t *world,
    ecs_table_t *table);

/* Reset a table to its initial state */
void flecs_table_reset(
    ecs_world_t *world,
    ecs_table_t *table);

/* Clear all entities from the table. Do not invoke OnRemove systems */
void flecs_table_clear_entities_silent(
    ecs_world_t *world,
    ecs_table_t *table);

/* Clear table data. Don't call OnRemove handlers. */
void flecs_table_clear_data(
    ecs_world_t *world,
    ecs_table_t *table,
    ecs_data_t *data);    

/* Return number of entities in data */
int32_t flecs_table_data_count(
    const ecs_data_t *data);

/* Add a new entry to the table for the specified entity */
int32_t flecs_table_append(
    ecs_world_t *world,
    ecs_table_t *table,
    ecs_entity_t entity,
    ecs_record_t *record,
    bool construct,
    bool on_add);

/* Delete an entity from the table. */
void flecs_table_delete(
    ecs_world_t *world,
    ecs_table_t *table,
    int32_t index,
    bool destruct);

/* Make sure table records are in correct table cache list */
bool flecs_table_records_update_empty(
    ecs_table_t *table);

/* Move a row from one table to another */
void flecs_table_move(
    ecs_world_t *world,
    ecs_entity_t dst_entity,
    ecs_entity_t src_entity,
    ecs_table_t *new_table,
    int32_t new_index,
    ecs_table_t *old_table,
    int32_t old_index,
    bool construct);

/* Grow table with specified number of records. Populate table with entities,
 * starting from specified entity id. */
int32_t flecs_table_appendn(
    ecs_world_t *world,
    ecs_table_t *table,
    ecs_data_t *data,
    int32_t count,
    const ecs_entity_t *ids);

/* Set table to a fixed size. Useful for preallocating memory in advance. */
void flecs_table_set_size(
    ecs_world_t *world,
    ecs_table_t *table,
    ecs_data_t *data,
    int32_t count);

/* Shrink table to contents */
bool flecs_table_shrink(
    ecs_world_t *world,
    ecs_table_t *table);

/* Get dirty state for table columns */
int32_t* flecs_table_get_dirty_state(
    ecs_world_t *world,
    ecs_table_t *table);

/* Initialize root table */
void flecs_init_root_table(
    ecs_world_t *world);

/* Unset components in table */
void flecs_table_remove_actions(
    ecs_world_t *world,
    ecs_table_t *table);

/* Free table */
void flecs_table_free(
    ecs_world_t *world,
    ecs_table_t *table); 

/* Free table */
void flecs_table_free_type(
    ecs_world_t *world,
    ecs_table_t *table);     
    
/* Replace data */
void flecs_table_replace_data(
    ecs_world_t *world,
    ecs_table_t *table,
    ecs_data_t *data);

/* Merge data of one table into another table */
void flecs_table_merge(
    ecs_world_t *world,
    ecs_table_t *new_table,
    ecs_table_t *old_table,
    ecs_data_t *new_data,
    ecs_data_t *old_data);

void flecs_table_swap(
    ecs_world_t *world,
    ecs_table_t *table,
    int32_t row_1,
    int32_t row_2);

ecs_table_t *flecs_table_traverse_add(
    ecs_world_t *world,
    ecs_table_t *table,
    ecs_id_t *id_ptr,
    ecs_table_diff_t *diff);

ecs_table_t *flecs_table_traverse_remove(
    ecs_world_t *world,
    ecs_table_t *table,
    ecs_id_t *id_ptr,
    ecs_table_diff_t *diff);

void flecs_table_mark_dirty(
    ecs_world_t *world,
    ecs_table_t *table,
    ecs_entity_t component);

void flecs_table_notify(
    ecs_world_t *world,
    ecs_table_t *table,
    ecs_table_event_t *event);

void flecs_table_clear_edges(
    ecs_world_t *world,
    ecs_table_t *table);

void flecs_table_delete_entities(
    ecs_world_t *world,
    ecs_table_t *table);

ecs_vec_t *ecs_table_column_for_id(
    const ecs_world_t *world,
    const ecs_table_t *table,
    ecs_id_t id);

int32_t flecs_table_column_to_union_index(
    const ecs_table_t *table,
    int32_t column);

/* Increase refcount of table (prevents deletion) */
void flecs_table_claim(
    ecs_world_t *world, 
    ecs_table_t *table);

/* Decreases refcount of table (may delete) */
bool flecs_table_release(
    ecs_world_t *world, 
    ecs_table_t *table);

/* Increase observer count of table */
void flecs_table_traversable_add(
    ecs_table_t *table,
    int32_t value);

/* Table diff builder, used to build id lists that indicate the difference in
 * ids between two tables. */
void flecs_table_diff_builder_init(
    ecs_world_t *world,
    ecs_table_diff_builder_t *builder);

void flecs_table_diff_builder_fini(
    ecs_world_t *world,
    ecs_table_diff_builder_t *builder);

void flecs_table_diff_builder_clear(
    ecs_table_diff_builder_t *builder);

void flecs_table_diff_build_append_table(
    ecs_world_t *world,
    ecs_table_diff_builder_t *dst,
    ecs_table_diff_t *src);

void flecs_table_diff_build(
    ecs_world_t *world,
    ecs_table_diff_builder_t *builder,
    ecs_table_diff_t *diff,
    int32_t added_offset,
    int32_t removed_offset);

void flecs_table_diff_build_noalloc(
    ecs_table_diff_builder_t *builder,
    ecs_table_diff_t *diff);

#endif

/**
 * @file poly.h
 * @brief Functions for managing poly objects.
 */

#ifndef FLECS_POLY_H
#define FLECS_POLY_H

#include <stddef.h>

/* Initialize poly */
void* _ecs_poly_init(
    ecs_poly_t *object,
    int32_t kind,
    ecs_size_t size,
    ecs_mixins_t *mixins);

#define ecs_poly_init(object, type)\
    _ecs_poly_init(object, type##_magic, sizeof(type), &type##_mixins)

/* Deinitialize object for specified type */
void _ecs_poly_fini(
    ecs_poly_t *object,
    int32_t kind);

#define ecs_poly_fini(object, type)\
    _ecs_poly_fini(object, type##_magic)

/* Utility functions for creating an object on the heap */
#define ecs_poly_new(type)\
    (type*)ecs_poly_init(ecs_os_calloc_t(type), type)

#define ecs_poly_free(obj, type)\
    ecs_poly_fini(obj, type);\
    ecs_os_free(obj)

/* Get or create poly component for an entity */
EcsPoly* _ecs_poly_bind(
    ecs_world_t *world,
    ecs_entity_t entity,
    ecs_entity_t tag);

#define ecs_poly_bind(world, entity, T) \
    _ecs_poly_bind(world, entity, T##_tag)

void _ecs_poly_modified(
    ecs_world_t *world,
    ecs_entity_t entity,
    ecs_entity_t tag);

#define ecs_poly_modified(world, entity, T) \
    _ecs_poly_modified(world, entity, T##_tag)

/* Get poly component for an entity */
const EcsPoly* _ecs_poly_bind_get(
    const ecs_world_t *world,
    ecs_entity_t entity,
    ecs_entity_t tag);

#define ecs_poly_bind_get(world, entity, T) \
    _ecs_poly_bind_get(world, entity, T##_tag)

ecs_poly_t* _ecs_poly_get(
    const ecs_world_t *world,
    ecs_entity_t entity,
    ecs_entity_t tag);

#define ecs_poly_get(world, entity, T) \
    ((T*)_ecs_poly_get(world, entity, T##_tag))

/* Utilities for testing/asserting an object type */
#ifndef FLECS_NDEBUG
void* _ecs_poly_assert(
    const ecs_poly_t *object,
    int32_t type,
    const char *file,
    int32_t line);

#define ecs_poly_assert(object, type)\
    _ecs_poly_assert(object, type##_magic, __FILE__, __LINE__)

#define ecs_poly(object, T) ((T*)ecs_poly_assert(object, T))
#else
#define ecs_poly_assert(object, type)
#define ecs_poly(object, T) ((T*)object)
#endif

/* Utility functions for getting a mixin from an object */
ecs_iterable_t* ecs_get_iterable(
    const ecs_poly_t *poly);

ecs_observable_t* ecs_get_observable(
    const ecs_poly_t *object);

ecs_poly_dtor_t* ecs_get_dtor(
    const ecs_poly_t *poly);

#endif

/**
 * @file stage.h
 * @brief Stage functions.
 */

#ifndef FLECS_STAGE_H
#define FLECS_STAGE_H

/* Initialize stage data structures */
void flecs_stage_init(
    ecs_world_t *world,
    ecs_stage_t *stage);

/* Deinitialize stage */
void flecs_stage_fini(
    ecs_world_t *world,
    ecs_stage_t *stage);

/* Post-frame merge actions */
void flecs_stage_merge_post_frame(
    ecs_world_t *world,
    ecs_stage_t *stage);  

bool flecs_defer_cmd(
    ecs_stage_t *stage);

bool flecs_defer_begin(
    ecs_world_t *world,
    ecs_stage_t *stage);

bool flecs_defer_modified(
    ecs_stage_t *stage,
    ecs_entity_t entity,
    ecs_entity_t component);

bool flecs_defer_clone(
    ecs_stage_t *stage,
    ecs_entity_t entity,
    ecs_entity_t src,
    bool clone_value);

bool flecs_defer_bulk_new(
    ecs_world_t *world,
    ecs_stage_t *stage,
    int32_t count,
    ecs_id_t id,
    const ecs_entity_t **ids_out);

bool flecs_defer_path(
    ecs_stage_t *stage,
    ecs_entity_t parent,
    ecs_entity_t entity,
    const char *name);

bool flecs_defer_delete(
    ecs_stage_t *stage,
    ecs_entity_t entity);

bool flecs_defer_clear(
    ecs_stage_t *stage,
    ecs_entity_t entity);

bool flecs_defer_on_delete_action(
    ecs_stage_t *stage,
    ecs_id_t id,
    ecs_entity_t action);

bool flecs_defer_enable(
    ecs_stage_t *stage,
    ecs_entity_t entity,
    ecs_entity_t component,
    bool enable);    

bool flecs_defer_add(
    ecs_stage_t *stage,
    ecs_entity_t entity,
    ecs_id_t id);

bool flecs_defer_remove(
    ecs_stage_t *stage,
    ecs_entity_t entity,
    ecs_id_t id);

void* flecs_defer_set(
    ecs_world_t *world,
    ecs_stage_t *stage,
    ecs_cmd_kind_t op_kind,
    ecs_entity_t entity,
    ecs_entity_t component,
    ecs_size_t size,
    void *value,
    bool need_value);

bool flecs_defer_end(
    ecs_world_t *world,
    ecs_stage_t *stage);

bool flecs_defer_purge(
    ecs_world_t *world,
    ecs_stage_t *stage);

#endif

/**
 * @file world.c
 * @brief World-level API.
 */

#ifndef FLECS_WORLD_H
#define FLECS_WORLD_H

/* Get current stage */
ecs_stage_t* flecs_stage_from_world(
    ecs_world_t **world_ptr);

/* Get current thread-specific stage from readonly world */
const ecs_stage_t* flecs_stage_from_readonly_world(
    const ecs_world_t *world);

/* Get component callbacks */
const ecs_type_info_t *flecs_type_info_get(
    const ecs_world_t *world,
    ecs_entity_t component);

/* Get or create component callbacks */
ecs_type_info_t* flecs_type_info_ensure(
    ecs_world_t *world,
    ecs_entity_t component);

bool flecs_type_info_init_id(
    ecs_world_t *world,
    ecs_entity_t component,
    ecs_size_t size,
    ecs_size_t alignment,
    const ecs_type_hooks_t *li);

#define flecs_type_info_init(world, T, ...)\
    flecs_type_info_init_id(world, ecs_id(T), ECS_SIZEOF(T), ECS_ALIGNOF(T),\
        &(ecs_type_hooks_t)__VA_ARGS__)

void flecs_type_info_fini(
    ecs_type_info_t *ti);

void flecs_type_info_free(
    ecs_world_t *world,
    ecs_entity_t component);

void flecs_eval_component_monitors(
    ecs_world_t *world);

void flecs_monitor_mark_dirty(
    ecs_world_t *world,
    ecs_entity_t id);

void flecs_monitor_register(
    ecs_world_t *world,
    ecs_entity_t id,
    ecs_query_t *query);

void flecs_monitor_unregister(
    ecs_world_t *world,
    ecs_entity_t id,
    ecs_query_t *query);

void flecs_notify_tables(
    ecs_world_t *world,
    ecs_id_t id,
    ecs_table_event_t *event);

void flecs_register_table(
    ecs_world_t *world,
    ecs_table_t *table);

void flecs_unregister_table(
    ecs_world_t *world,
    ecs_table_t *table);

void flecs_table_set_empty(
    ecs_world_t *world,
    ecs_table_t *table);

void flecs_delete_table(
    ecs_world_t *world,
    ecs_table_t *table);

void flecs_process_pending_tables(
    const ecs_world_t *world);

/* Suspend/resume readonly state. To fully support implicit registration of
 * components, it should be possible to register components while the world is
 * in readonly mode. It is not uncommon that a component is used first from
 * within a system, which are often ran while in readonly mode.
 * 
 * Suspending readonly mode is only allowed when the world is not multithreaded.
 * When a world is multithreaded, it is not safe to (even temporarily) leave
 * readonly mode, so a multithreaded application should always explicitly
 * register components in advance. 
 * 
 * These operations also suspend deferred mode.
 */
typedef struct ecs_suspend_readonly_state_t {
    bool is_readonly;
    bool is_deferred;
    int32_t defer_count;
    ecs_entity_t scope;
    ecs_entity_t with;
    ecs_vec_t commands;
    ecs_stack_t defer_stack;
    ecs_stage_t *stage;
} ecs_suspend_readonly_state_t;

ecs_world_t* flecs_suspend_readonly(
    const ecs_world_t *world,
    ecs_suspend_readonly_state_t *state);

void flecs_resume_readonly(
    ecs_world_t *world,
    ecs_suspend_readonly_state_t *state);

/* Convenience macro's for world allocator */
#define flecs_walloc(world, size)\
    flecs_alloc(&world->allocator, size)
#define flecs_walloc_n(world, T, count)\
    flecs_alloc_n(&world->allocator, T, count)
#define flecs_wcalloc(world, size)\
    flecs_calloc(&world->allocator, size)
#define flecs_wcalloc_n(world, T, count)\
    flecs_calloc_n(&world->allocator, T, count)
#define flecs_wfree(world, size, ptr)\
    flecs_free(&world->allocator, size, ptr)
#define flecs_wfree_n(world, T, count, ptr)\
    flecs_free_n(&world->allocator, T, count, ptr)
#define flecs_wrealloc(world, size_dst, size_src, ptr)\
    flecs_realloc(&world->allocator, size_dst, size_src, ptr)
#define flecs_wrealloc_n(world, T, count_dst, count_src, ptr)\
    flecs_realloc_n(&world->allocator, T, count_dst, count_src, ptr)
#define flecs_wdup(world, size, ptr)\
    flecs_dup(&world->allocator, size, ptr)
#define flecs_wdup_n(world, T, count, ptr)\
    flecs_dup_n(&world->allocator, T, count, ptr)

#endif

/**
 * @file datastructures/qsort.h
 * @brief Quicksort implementation.
 */

/* From: https://github.com/svpv/qsort/blob/master/qsort.h 
 * Use custom qsort implementation rather than relying on the version in libc to
 * ensure that results are consistent across platforms.
 */

/*
 * Copyright (c) 2013, 2017 Alexey Tourbin
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.  IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 * SOFTWARE.
 */

/*
 * This is a traditional Quicksort implementation which mostly follows
 * [Sedgewick 1978].  Sorting is performed entirely on array indices,
 * while actual access to the array elements is abstracted out with the
 * user-defined `LESS` and `SWAP` primitives.
 *
 * Synopsis:
 *	QSORT(N, LESS, SWAP);
 * where
 *	N - the number of elements in A[];
 *	LESS(i, j) - compares A[i] to A[j];
 *	SWAP(i, j) - exchanges A[i] with A[j].
 */

#ifndef QSORT_H
#define QSORT_H

/* Sort 3 elements. */
#define Q_SORT3(q_a1, q_a2, q_a3, Q_LESS, Q_SWAP) \
do {					\
    if (Q_LESS(q_a2, q_a1)) {		\
	if (Q_LESS(q_a3, q_a2))		\
	    Q_SWAP(q_a1, q_a3);		\
	else {				\
	    Q_SWAP(q_a1, q_a2);		\
	    if (Q_LESS(q_a3, q_a2))	\
		Q_SWAP(q_a2, q_a3);	\
	}				\
    }					\
    else if (Q_LESS(q_a3, q_a2)) {	\
	Q_SWAP(q_a2, q_a3);		\
	if (Q_LESS(q_a2, q_a1))		\
	    Q_SWAP(q_a1, q_a2);		\
    }					\
} while (0)

/* Partition [q_l,q_r] around a pivot.  After partitioning,
 * [q_l,q_j] are the elements that are less than or equal to the pivot,
 * while [q_i,q_r] are the elements greater than or equal to the pivot. */
#define Q_PARTITION(q_l, q_r, q_i, q_j, Q_UINT, Q_LESS, Q_SWAP)		\
do {									\
    /* The middle element, not to be confused with the median. */	\
    Q_UINT q_m = q_l + ((q_r - q_l) >> 1);				\
    /* Reorder the second, the middle, and the last items.		\
     * As [Edelkamp Weiss 2016] explain, using the second element	\
     * instead of the first one helps avoid bad behaviour for		\
     * decreasingly sorted arrays.  This method is used in recent	\
     * versions of gcc's std::sort, see gcc bug 58437#c13, although	\
     * the details are somewhat different (cf. #c14). */		\
    Q_SORT3(q_l + 1, q_m, q_r, Q_LESS, Q_SWAP);				\
    /* Place the median at the beginning. */				\
    Q_SWAP(q_l, q_m);							\
    /* Partition [q_l+2, q_r-1] around the median which is in q_l.	\
     * q_i and q_j are initially off by one, they get decremented	\
     * in the do-while loops. */					\
    q_i = q_l + 1; q_j = q_r;						\
    while (1) {								\
	do q_i++; while (Q_LESS(q_i, q_l));				\
	do q_j--; while (Q_LESS(q_l, q_j));				\
	if (q_i >= q_j) break; /* Sedgewick says "until j < i" */	\
	Q_SWAP(q_i, q_j);						\
    }									\
    /* Compensate for the i==j case. */					\
    q_i = q_j + 1;							\
    /* Put the median to its final place. */				\
    Q_SWAP(q_l, q_j);							\
    /* The median is not part of the left subfile. */			\
    q_j--;								\
} while (0)

/* Insertion sort is applied to small subfiles - this is contrary to
 * Sedgewick's suggestion to run a separate insertion sort pass after
 * the partitioning is done.  The reason I don't like a separate pass
 * is that it triggers extra comparisons, because it can't see that the
 * medians are already in their final positions and need not be rechecked.
 * Since I do not assume that comparisons are cheap, I also do not try
 * to eliminate the (q_j > q_l) boundary check. */
#define Q_INSERTION_SORT(q_l, q_r, Q_UINT, Q_LESS, Q_SWAP)		\
do {									\
    Q_UINT q_i, q_j;							\
    /* For each item starting with the second... */			\
    for (q_i = q_l + 1; q_i <= q_r; q_i++)				\
    /* move it down the array so that the first part is sorted. */	\
    for (q_j = q_i; q_j > q_l && (Q_LESS(q_j, q_j - 1)); q_j--)		\
	Q_SWAP(q_j, q_j - 1);						\
} while (0)

/* When the size of [q_l,q_r], i.e. q_r-q_l+1, is greater than or equal to
 * Q_THRESH, the algorithm performs recursive partitioning.  When the size
 * drops below Q_THRESH, the algorithm switches to insertion sort.
 * The minimum valid value is probably 5 (with 5 items, the second and
 * the middle items, the middle itself being rounded down, are distinct). */
#define Q_THRESH 16

/* The main loop. */
#define Q_LOOP(Q_UINT, Q_N, Q_LESS, Q_SWAP)				\
do {									\
    Q_UINT q_l = 0;							\
    Q_UINT q_r = (Q_N) - 1;						\
    Q_UINT q_sp = 0; /* the number of frames pushed to the stack */	\
    struct { Q_UINT q_l, q_r; }						\
	/* On 32-bit platforms, to sort a "char[3GB+]" array,		\
	 * it may take full 32 stack frames.  On 64-bit CPUs,		\
	 * though, the address space is limited to 48 bits.		\
	 * The usage is further reduced if Q_N has a 32-bit type. */	\
	q_st[sizeof(Q_UINT) > 4 && sizeof(Q_N) > 4 ? 48 : 32];		\
    while (1) {								\
	if (q_r - q_l + 1 >= Q_THRESH) {				\
	    Q_UINT q_i, q_j;						\
	    Q_PARTITION(q_l, q_r, q_i, q_j, Q_UINT, Q_LESS, Q_SWAP);	\
	    /* Now have two subfiles: [q_l,q_j] and [q_i,q_r].		\
	     * Dealing with them depends on which one is bigger. */	\
	    if (q_j - q_l >= q_r - q_i)					\
		Q_SUBFILES(q_l, q_j, q_i, q_r);				\
	    else							\
		Q_SUBFILES(q_i, q_r, q_l, q_j);				\
	}								\
	else {								\
	    Q_INSERTION_SORT(q_l, q_r, Q_UINT, Q_LESS, Q_SWAP);		\
	    /* Pop subfiles from the stack, until it gets empty. */	\
	    if (q_sp == 0) break;					\
	    q_sp--;							\
	    q_l = q_st[q_sp].q_l;					\
	    q_r = q_st[q_sp].q_r;					\
	}								\
    }									\
} while (0)

/* The missing part: dealing with subfiles.
 * Assumes that the first subfile is not smaller than the second. */
#define Q_SUBFILES(q_l1, q_r1, q_l2, q_r2)				\
do {									\
    /* If the second subfile is only a single element, it needs		\
     * no further processing.  The first subfile will be processed	\
     * on the next iteration (both subfiles cannot be only a single	\
     * element, due to Q_THRESH). */					\
    if (q_l2 == q_r2) {							\
	q_l = q_l1;							\
	q_r = q_r1;							\
    }									\
    else {								\
	/* Otherwise, both subfiles need processing.			\
	 * Push the larger subfile onto the stack. */			\
	q_st[q_sp].q_l = q_l1;						\
	q_st[q_sp].q_r = q_r1;						\
	q_sp++;								\
	/* Process the smaller subfile on the next iteration. */	\
	q_l = q_l2;							\
	q_r = q_r2;							\
    }									\
} while (0)

/* And now, ladies and gentlemen, may I proudly present to you... */
#define QSORT(Q_N, Q_LESS, Q_SWAP)					\
do {									\
    if ((Q_N) > 1)							\
	/* We could check sizeof(Q_N) and use "unsigned", but at least	\
	 * on x86_64, this has the performance penalty of up to 5%. */	\
	Q_LOOP(ecs_size_t, Q_N, Q_LESS, Q_SWAP);			\
} while (0)

void ecs_qsort(
    void *base, 
    ecs_size_t nitems, 
    ecs_size_t size, 
    int (*compar)(const void *, const void*));

#define ecs_qsort_t(base, nitems, T, compar) \
    ecs_qsort(base, nitems, ECS_SIZEOF(T), compar)

#endif

/**
 * @file datastructures/name_index.h
 * @brief Data structure for resolving 64bit keys by string (name).
 */

#ifndef FLECS_NAME_INDEX_H
#define FLECS_NAME_INDEX_H

void flecs_name_index_init(
    ecs_hashmap_t *hm,
    ecs_allocator_t *allocator);

void flecs_name_index_init_if(
    ecs_hashmap_t *hm,
    ecs_allocator_t *allocator);

bool flecs_name_index_is_init(
    const ecs_hashmap_t *hm);

ecs_hashmap_t* flecs_name_index_new(
    ecs_world_t *world,
    ecs_allocator_t *allocator);

void flecs_name_index_fini(
    ecs_hashmap_t *map);

void flecs_name_index_free(
    ecs_hashmap_t *map);

ecs_hashmap_t* flecs_name_index_copy(
    ecs_hashmap_t *dst);

ecs_hashed_string_t flecs_get_hashed_string(
    const char *name,
    ecs_size_t length,
    uint64_t hash);

const uint64_t* flecs_name_index_find_ptr(
    const ecs_hashmap_t *map,
    const char *name,
    ecs_size_t length,
    uint64_t hash);

uint64_t flecs_name_index_find(
    const ecs_hashmap_t *map,
    const char *name,
    ecs_size_t length,
    uint64_t hash);

void flecs_name_index_ensure(
    ecs_hashmap_t *map,
    uint64_t id,
    const char *name,
    ecs_size_t length,
    uint64_t hash);

void flecs_name_index_remove(
    ecs_hashmap_t *map,
    uint64_t id,
    uint64_t hash);

void flecs_name_index_update_name(
    ecs_hashmap_t *map,
    uint64_t e,
    uint64_t hash,
    const char *name);

#endif



////////////////////////////////////////////////////////////////////////////////
//// Bootstrap API
////////////////////////////////////////////////////////////////////////////////

/* Bootstrap world */
void flecs_bootstrap(
    ecs_world_t *world);

#define flecs_bootstrap_component(world, id_)\
    ecs_component_init(world, &(ecs_component_desc_t){\
        .entity = ecs_entity(world, { .id = ecs_id(id_), .name = #id_, .symbol = #id_ }),\
        .type.size = sizeof(id_),\
        .type.alignment = ECS_ALIGNOF(id_)\
    });

#define flecs_bootstrap_tag(world, name)\
    ecs_ensure(world, name);\
    ecs_add_id(world, name, EcsFinal);\
    ecs_add_pair(world, name, EcsChildOf, ecs_get_scope(world));\
    ecs_set(world, name, EcsComponent, {.size = 0});\
    ecs_set_name(world, name, (char*)&#name[ecs_os_strlen(world->info.name_prefix)]);\
    ecs_set_symbol(world, name, #name)

/* Bootstrap functions for other parts in the code */
void flecs_bootstrap_hierarchy(ecs_world_t *world);


////////////////////////////////////////////////////////////////////////////////
//// Entity API
////////////////////////////////////////////////////////////////////////////////

/* Mark an entity as being watched. This is used to trigger automatic rematching
 * when entities used in system expressions change their components. */
void flecs_add_flag(
    ecs_world_t *world,
    ecs_entity_t entity,
    uint32_t flag);

void flecs_record_add_flag(
    ecs_record_t *record,
    uint32_t flag);

ecs_entity_t flecs_get_oneof(
    const ecs_world_t *world,
    ecs_entity_t e);

void flecs_notify_on_remove(
    ecs_world_t *world,
    ecs_table_t *table,
    ecs_table_t *other_table,
    int32_t row,
    int32_t count,
    const ecs_type_t *diff);

void flecs_notify_on_set(
    ecs_world_t *world,
    ecs_table_t *table,
    int32_t row,
    int32_t count,
    ecs_type_t *type,
    bool owned);

int32_t flecs_relation_depth(
    const ecs_world_t *world,
    ecs_entity_t r,
    const ecs_table_t *table);

void flecs_instantiate(
    ecs_world_t *world,
    ecs_entity_t base,
    ecs_table_t *table,
    int32_t row,
    int32_t count);

void* flecs_get_base_component(
    const ecs_world_t *world,
    ecs_table_t *table,
    ecs_id_t id,
    ecs_id_record_t *table_index,
    int32_t recur_depth);

void flecs_invoke_hook(
    ecs_world_t *world,
    ecs_table_t *table,
    int32_t count,
    int32_t row,
    ecs_entity_t *entities,
    void *ptr,
    ecs_id_t id,
    const ecs_type_info_t *ti,
    ecs_entity_t event,
    ecs_iter_action_t hook);

////////////////////////////////////////////////////////////////////////////////
//// Query API
////////////////////////////////////////////////////////////////////////////////

/* Match table with term */
bool flecs_term_match_table(
    ecs_world_t *world,
    const ecs_term_t *term,
    const ecs_table_t *table,
    ecs_id_t *id_out,
    int32_t *column_out,
    ecs_entity_t *subject_out,
    int32_t *match_indices,
    bool first,
    ecs_flags32_t iter_flags);

/* Match table with filter */
bool flecs_filter_match_table(
    ecs_world_t *world,
    const ecs_filter_t *filter,
    const ecs_table_t *table,
    ecs_id_t *ids,
    int32_t *columns,
    ecs_entity_t *sources,
    int32_t *match_indices,
    int32_t *matches_left,
    bool first,
    int32_t skip_term,
    ecs_flags32_t iter_flags);

ecs_iter_t flecs_filter_iter_w_flags(
    const ecs_world_t *stage,
    const ecs_filter_t *filter,
    ecs_flags32_t flags);

void flecs_query_notify(
    ecs_world_t *world,
    ecs_query_t *query,
    ecs_query_event_t *event);

ecs_id_t flecs_to_public_id(
    ecs_id_t id);

ecs_id_t flecs_from_public_id(
    ecs_world_t *world,
    ecs_id_t id);

void flecs_filter_apply_iter_flags(
    ecs_iter_t *it,
    const ecs_filter_t *filter);

////////////////////////////////////////////////////////////////////////////////
//// Safe(r) integer casting
////////////////////////////////////////////////////////////////////////////////

#define FLECS_CONVERSION_ERR(T, value)\
    "illegal conversion from value " #value " to type " #T

#define flecs_signed_char__ (CHAR_MIN < 0)
#define flecs_signed_short__ true
#define flecs_signed_int__ true
#define flecs_signed_long__ true
#define flecs_signed_size_t__ false
#define flecs_signed_int8_t__ true
#define flecs_signed_int16_t__ true
#define flecs_signed_int32_t__ true
#define flecs_signed_int64_t__ true
#define flecs_signed_intptr_t__ true
#define flecs_signed_uint8_t__ false
#define flecs_signed_uint16_t__ false
#define flecs_signed_uint32_t__ false
#define flecs_signed_uint64_t__ false
#define flecs_signed_uintptr_t__ false
#define flecs_signed_ecs_size_t__ true
#define flecs_signed_ecs_entity_t__ false

uint64_t _flecs_ito(
    size_t dst_size,
    bool dst_signed,
    bool lt_zero,
    uint64_t value,
    const char *err);

#ifndef FLECS_NDEBUG
#define flecs_ito(T, value)\
    (T)_flecs_ito(\
        sizeof(T),\
        flecs_signed_##T##__,\
        (value) < 0,\
        (uint64_t)(value),\
        FLECS_CONVERSION_ERR(T, (value)))

#define flecs_uto(T, value)\
    (T)_flecs_ito(\
        sizeof(T),\
        flecs_signed_##T##__,\
        false,\
        (uint64_t)(value),\
        FLECS_CONVERSION_ERR(T, (value)))
#else
#define flecs_ito(T, value) (T)(value)
#define flecs_uto(T, value) (T)(value)
#endif

#define flecs_itosize(value) flecs_ito(size_t, (value))
#define flecs_utosize(value) flecs_uto(ecs_size_t, (value))
#define flecs_itoi16(value) flecs_ito(int16_t, (value))
#define flecs_itoi32(value) flecs_ito(int32_t, (value))

////////////////////////////////////////////////////////////////////////////////
//// Entity filter
////////////////////////////////////////////////////////////////////////////////

void flecs_entity_filter_init(
    ecs_world_t *world,
    ecs_entity_filter_t **entity_filter,
    const ecs_filter_t *filter,
    const ecs_table_t *table,
    ecs_id_t *ids,
    int32_t *columns);

void flecs_entity_filter_fini(
    ecs_world_t *world,
    ecs_entity_filter_t *entity_filter);

int flecs_entity_filter_next(
    ecs_entity_filter_iter_t *it);

////////////////////////////////////////////////////////////////////////////////
//// Utilities
////////////////////////////////////////////////////////////////////////////////

uint64_t flecs_hash(
    const void *data,
    ecs_size_t length);

uint64_t flecs_wyhash(
    const void *data,
    ecs_size_t length);

/* Get next power of 2 */
int32_t flecs_next_pow_of_2(
    int32_t n);

/* Convert 64bit value to ecs_record_t type. ecs_record_t is stored as 64bit int in the
 * entity index */
ecs_record_t flecs_to_row(
    uint64_t value);

/* Get 64bit integer from ecs_record_t */
uint64_t flecs_from_row(
    ecs_record_t record);

/* Convert a symbol name to an entity name by removing the prefix */
const char* flecs_name_from_symbol(
    ecs_world_t *world,
    const char *type_name);

/* Compare function for entity ids */
int flecs_entity_compare(
    ecs_entity_t e1, 
    const void *ptr1, 
    ecs_entity_t e2, 
    const void *ptr2); 

/* Compare function for entity ids which can be used with qsort */
int flecs_entity_compare_qsort(
    const void *e1,
    const void *e2);

bool flecs_name_is_id(
    const char *name);

ecs_entity_t flecs_name_to_id(
    const ecs_world_t *world,
    const char *name);

/* Convert floating point to string */
char * ecs_ftoa(
    double f, 
    char * buf, 
    int precision);

uint64_t flecs_string_hash(
    const void *ptr);

void flecs_table_hashmap_init(
    ecs_world_t *world,
    ecs_hashmap_t *hm);

#define assert_func(cond) _assert_func(cond, #cond, __FILE__, __LINE__, __func__)
void _assert_func(
    bool cond,
    const char *cond_str,
    const char *file,
    int32_t line,
    const char *func);

void flecs_dump_backtrace(
    FILE *stream);

void flecs_colorize_buf(
    char *msg,
    bool enable_colors,
    ecs_strbuf_t *buf);

bool flecs_isident(
    char ch);

int32_t flecs_search_w_idr(
    const ecs_world_t *world,
    const ecs_table_t *table,
    ecs_id_t id,
    ecs_id_t *id_out,
    ecs_id_record_t *idr);

int32_t flecs_search_relation_w_idr(
    const ecs_world_t *world,
    const ecs_table_t *table,
    int32_t offset,
    ecs_id_t id,
    ecs_entity_t rel,
    ecs_flags32_t flags,
    ecs_entity_t *subject_out,
    ecs_id_t *id_out,
    struct ecs_table_record_t **tr_out,
    ecs_id_record_t *idr);

#endif


/* Table sanity check to detect storage issues. Only enabled in SANITIZE mode as
 * this can severly slow down many ECS operations. */
#ifdef FLECS_SANITIZE
static
void flecs_table_check_sanity(ecs_table_t *table) {
    int32_t size = ecs_vec_size(&table->data.entities);
    int32_t count = ecs_vec_count(&table->data.entities);
    
    ecs_assert(size == ecs_vec_size(&table->data.records), 
        ECS_INTERNAL_ERROR, NULL);
    ecs_assert(count == ecs_vec_count(&table->data.records), 
        ECS_INTERNAL_ERROR, NULL);

    int32_t i;
    int32_t sw_offset = table->_ ? table->_->sw_offset : 0;
    int32_t sw_count = table->_ ? table->_->sw_count : 0;
    int32_t bs_offset = table->_ ? table->_->bs_offset : 0;
    int32_t bs_count = table->_ ? table->_->bs_count : 0;
    int32_t type_count = table->type.count;
    ecs_id_t *ids = table->type.array;

    ecs_assert((sw_count + sw_offset) <= type_count, ECS_INTERNAL_ERROR, NULL);
    ecs_assert((bs_count + bs_offset) <= type_count, ECS_INTERNAL_ERROR, NULL);

    ecs_table_t *storage_table = table->storage_table;
    if (storage_table) {
        ecs_assert(table->storage_count == storage_table->type.count,
            ECS_INTERNAL_ERROR, NULL);
        ecs_assert(table->storage_ids == storage_table->type.array, 
            ECS_INTERNAL_ERROR, NULL);

        int32_t storage_count = table->storage_count;
        ecs_assert(type_count >= storage_count, ECS_INTERNAL_ERROR, NULL);

        int32_t *storage_map = table->storage_map;
        ecs_assert(storage_map != NULL, ECS_INTERNAL_ERROR, NULL);

        ecs_id_t *storage_ids = table->storage_ids;
        for (i = 0; i < type_count; i ++) {
            if (storage_map[i] != -1) {
                ecs_assert(ids[i] == storage_ids[storage_map[i]],
                    ECS_INTERNAL_ERROR, NULL);
            }
        }

        ecs_assert(table->data.columns != NULL, ECS_INTERNAL_ERROR, NULL);
        ecs_assert(table->type_info != NULL, ECS_INTERNAL_ERROR, NULL);

        for (i = 0; i < storage_count; i ++) {
            ecs_vec_t *column = &table->data.columns[i];
            ecs_assert(size == column->size, ECS_INTERNAL_ERROR, NULL);
            ecs_assert(count == column->count, ECS_INTERNAL_ERROR, NULL);
            int32_t storage_map_id = storage_map[i + type_count];
            ecs_assert(storage_map_id >= 0, ECS_INTERNAL_ERROR, NULL);
            ecs_assert(ids[storage_map_id] == storage_ids[i],
                ECS_INTERNAL_ERROR, NULL);
        }
    } else {
        ecs_assert(table->storage_count == 0, ECS_INTERNAL_ERROR, NULL);
        ecs_assert(table->storage_ids == NULL, ECS_INTERNAL_ERROR, NULL);
    }

    if (sw_count) {
        ecs_assert(table->_->sw_columns != NULL, 
            ECS_INTERNAL_ERROR, NULL);
        for (i = 0; i < sw_count; i ++) {
            ecs_switch_t *sw = &table->_->sw_columns[i];
            ecs_assert(ecs_vec_count(&sw->values) == count, 
                ECS_INTERNAL_ERROR, NULL);
            ecs_assert(ECS_PAIR_FIRST(ids[i + sw_offset]) == EcsUnion,
                ECS_INTERNAL_ERROR, NULL);
        }
    }

    if (bs_count) {
        ecs_assert(table->_->bs_columns != NULL, 
            ECS_INTERNAL_ERROR, NULL);
        for (i = 0; i < bs_count; i ++) {
            ecs_bitset_t *bs = &table->_->bs_columns[i];
            ecs_assert(flecs_bitset_count(bs) == count,
                ECS_INTERNAL_ERROR, NULL);
            ecs_assert(ECS_HAS_ID_FLAG(ids[i + bs_offset], TOGGLE),
                ECS_INTERNAL_ERROR, NULL);
        }
    }

    ecs_assert((table->_->traversable_count == 0) || 
        (table->flags & EcsTableHasTraversable), ECS_INTERNAL_ERROR, NULL);
}
#else
#define flecs_table_check_sanity(table)
#endif

/* Initialize the storage map for a table. A storage map is an integer array
 * that maps type indices to column indices and vice versa. */
static
void flecs_table_init_storage_map(
    ecs_world_t *world,
    ecs_table_t *table)
{
    ecs_assert(table != NULL, ECS_INTERNAL_ERROR, NULL);
    if (!table->storage_table) {
        return;
    }

    ecs_type_t type = table->type;
    ecs_id_t *ids = type.array;
    int32_t t, ids_count = type.count;
    ecs_id_t *storage_ids = table->storage_ids;
    int32_t s, storage_ids_count = table->storage_count;

    if (!ids_count) {
        table->storage_map = NULL;
        return;
    }

    table->storage_map = flecs_walloc_n(world, int32_t, 
        ids_count + storage_ids_count);

    int32_t *t2s = table->storage_map;
    int32_t *s2t = &table->storage_map[ids_count];

    for (s = 0, t = 0; (t < ids_count) && (s < storage_ids_count); ) {
        ecs_id_t id = ids[t];
        ecs_id_t storage_id = storage_ids[s];

        if (id == storage_id) {
            t2s[t] = s;
            s2t[s] = t;
        } else {
            t2s[t] = -1;
        }

        /* Ids can never get ahead of storage id, as ids are a superset of the
         * storage ids */
        ecs_assert(id <= storage_id, ECS_INTERNAL_ERROR, NULL);

        t += (id <= storage_id);
        s += (id == storage_id);
    }

    /* Storage ids is always a subset of ids, so all should be iterated */
    ecs_assert(s == storage_ids_count, ECS_INTERNAL_ERROR, NULL);

    /* Initialize remainder of type -> storage_type map */
    for (; (t < ids_count); t ++) {
        t2s[t] = -1;
    }
}

/* Set flags for type hooks so table operations can quickly check whether a
 * fast or complex operation that invokes hooks is required. */
static
ecs_flags32_t flecs_type_info_flags(
    const ecs_type_info_t *ti) 
{
    ecs_flags32_t flags = 0;

    if (ti->hooks.ctor) {
        flags |= EcsTableHasCtors;
    }
    if (ti->hooks.on_add) {
        flags |= EcsTableHasCtors;
    }
    if (ti->hooks.dtor) {
        flags |= EcsTableHasDtors;
    }
    if (ti->hooks.on_remove) {
        flags |= EcsTableHasDtors;
    }
    if (ti->hooks.copy) {
        flags |= EcsTableHasCopy;
    }
    if (ti->hooks.move) {
        flags |= EcsTableHasMove;
    }  

    return flags;  
}

/* Initialize array with cached pointers to type info. The type info array has
 * an element for each table column. Multiple tables may share the same type
 * info array, as long as they have the same components. */
static
void flecs_table_init_type_info(
    ecs_world_t *world,
    ecs_table_t *table)
{
    ecs_assert(table != NULL, ECS_INTERNAL_ERROR, NULL);
    ecs_assert(table->storage_table == table, ECS_INTERNAL_ERROR, NULL);
    ecs_assert(table->type_info == NULL, ECS_INTERNAL_ERROR, NULL);

    ecs_table_record_t *records = table->_->records;
    int32_t i, count = table->type.count;
    table->type_info = flecs_walloc_n(world, ecs_type_info_t*, count);

    for (i = 0; i < count; i ++) {
        ecs_table_record_t *tr = &records[i];
        ecs_id_record_t *idr = (ecs_id_record_t*)tr->hdr.cache;
        
        /* All ids in the storage table must be components with type info */
        const ecs_type_info_t *ti = idr->type_info;
        ecs_assert(ti != NULL, ECS_INTERNAL_ERROR, NULL);
        table->flags |= flecs_type_info_flags(ti);
        table->type_info[i] = (ecs_type_info_t*)ti;
    }
}

/* Find or create the storage table for a table. A storage table only contains
 * components, no tags. A table maintains a reference to its storage table as
 * this is used for sharing metadata such as the type info array. A storage 
 * table is found by taking a table type and removing all non-component ids. 
 * For example, for table
 *   [Position, Velocity, Npc]
 * the storage table would be
 *   [Position, Velocity]
 * assuming that Npc is a tag.
 */
static
void flecs_table_init_storage_table(
    ecs_world_t *world,
    ecs_table_t *table)
{
    if (table->storage_table) {
        return;
    }

    ecs_type_t type = table->type;
    int32_t i, count = type.count;
    ecs_id_t *ids = type.array;
    ecs_table_record_t *records = table->_->records;

    ecs_id_t array[FLECS_ID_DESC_MAX];
    ecs_type_t storage_ids = { .array = array };
    if (count > FLECS_ID_DESC_MAX) {
        storage_ids.array = flecs_walloc_n(world, ecs_id_t, count);
    }

    for (i = 0; i < count; i ++) {
        ecs_table_record_t *tr = &records[i];
        ecs_id_record_t *idr = (ecs_id_record_t*)tr->hdr.cache;
        ecs_id_t id = ids[i];

        if (idr->type_info != NULL) {
            storage_ids.array[storage_ids.count ++] = id;
        }
    }

    if (storage_ids.count && storage_ids.count != count) {
        ecs_table_t *storage_table = flecs_table_find_or_create(world, 
            &storage_ids);
        table->storage_table = storage_table;
        table->storage_count = flecs_ito(uint16_t, storage_ids.count);
        table->storage_ids = storage_table->type.array;
        table->type_info = storage_table->type_info;
        table->flags |= storage_table->flags;
        storage_table->_->refcount ++;
    } else if (storage_ids.count) {
        table->storage_table = table;
        table->storage_count = flecs_ito(uint16_t, count);
        table->storage_ids = type.array;
        flecs_table_init_type_info(world, table);
    }

    if (storage_ids.array != array) {
        flecs_wfree_n(world, ecs_id_t, count, storage_ids.array);
    }

    if (!table->storage_map) {
        flecs_table_init_storage_map(world, table);
    }
}

/* Initialize table column vectors */
void flecs_table_init_data(
    ecs_world_t *world,
    ecs_table_t *table)
{
    ecs_data_t *storage = &table->data;
    int32_t i, count = table->storage_count;

    ecs_vec_init_t(NULL, &storage->entities, ecs_entity_t, 0);
    ecs_vec_init_t(NULL, &storage->records, ecs_record_t*, 0);

    if (count) {
        ecs_vec_t *columns = flecs_wcalloc_n(world, ecs_vec_t, count);
        storage->columns = columns;
#ifdef FLECS_DEBUG
        ecs_type_info_t **ti = table->type_info;
        for (i = 0; i < count; i ++) {
            ecs_vec_init(NULL, &columns[i], ti[i]->size, 0);
        }
#endif
    }

    ecs_table__t *meta = table->_;
    int32_t sw_count = meta->sw_count;
    int32_t bs_count = meta->bs_count;

    if (sw_count) {
        meta->sw_columns = flecs_wcalloc_n(world, ecs_switch_t, sw_count);
        for (i = 0; i < sw_count; i ++) {
            flecs_switch_init(&meta->sw_columns[i], 
                &world->allocator, 0);
        }
    }

    if (bs_count) {
        meta->bs_columns = flecs_wcalloc_n(world, ecs_bitset_t, bs_count);
        for (i = 0; i < bs_count; i ++) {
            flecs_bitset_init(&meta->bs_columns[i]);
        }
    }
}

/* Initialize table flags. Table flags are used in lots of scenarios to quickly
 * check the features of a table without having to inspect the table type. Table
 * flags are typically used to early-out of potentially expensive operations. */
static
void flecs_table_init_flags(
    ecs_world_t *world,
    ecs_table_t *table)
{
    ecs_id_t *ids = table->type.array;
    int32_t count = table->type.count;

    int32_t i;
    for (i = 0; i < count; i ++) {
        ecs_id_t id = ids[i];

        if (id <= EcsLastInternalComponentId) {
            table->flags |= EcsTableHasBuiltins;
        }

        if (id == EcsModule) {
            table->flags |= EcsTableHasBuiltins;
            table->flags |= EcsTableHasModule;
        } else if (id == EcsPrefab) {
            table->flags |= EcsTableIsPrefab;
        } else if (id == EcsDisabled) {
            table->flags |= EcsTableIsDisabled;
        } else {
            if (ECS_IS_PAIR(id)) {
                ecs_entity_t r = ECS_PAIR_FIRST(id);

                table->flags |= EcsTableHasPairs;

                if (r == EcsIsA) {
                    table->flags |= EcsTableHasIsA;
                } else if (r == EcsChildOf) {
                    table->flags |= EcsTableHasChildOf;
                    ecs_entity_t obj = ecs_pair_second(world, id);
                    ecs_assert(obj != 0, ECS_INTERNAL_ERROR, NULL);

                    if (obj == EcsFlecs || obj == EcsFlecsCore || 
                        ecs_has_id(world, obj, EcsModule)) 
                    {
                        /* If table contains entities that are inside one of the 
                         * builtin modules, it contains builtin entities */
                        table->flags |= EcsTableHasBuiltins;
                        table->flags |= EcsTableHasModule;
                    }
                } else if (id == ecs_pair_t(EcsIdentifier, EcsName)) {
                    table->flags |= EcsTableHasName;
                } else if (r == EcsUnion) {
                    ecs_table__t *meta = table->_;
                    table->flags |= EcsTableHasUnion;

                    if (!meta->sw_count) {
                        meta->sw_offset = flecs_ito(int16_t, i);
                    }
                    meta->sw_count ++;
                } else if (r == ecs_id(EcsTarget)) {
                    ecs_table__t *meta = table->_;
                    table->flags |= EcsTableHasTarget;
                    meta->ft_offset = flecs_ito(int16_t, i);
                } else if (r == ecs_id(EcsPoly)) {
                    table->flags |= EcsTableHasBuiltins;
                }
            } else {
                if (ECS_HAS_ID_FLAG(id, TOGGLE)) {
                    ecs_table__t *meta = table->_;
                    table->flags |= EcsTableHasToggle;

                    if (!meta->bs_count) {
                        meta->bs_offset = flecs_ito(int16_t, i);
                    }
                    meta->bs_count ++;
                }
                if (ECS_HAS_ID_FLAG(id, OVERRIDE)) {
                    table->flags |= EcsTableHasOverrides;
                }
            }
        } 
    }
}

/* Utility function that appends an element to the table record array */
static
void flecs_table_append_to_records(
    ecs_world_t *world,
    ecs_table_t *table,
    ecs_vec_t *records,
    ecs_id_t id,
    int32_t column)
{
    /* To avoid a quadratic search, use the O(1) lookup that the index
     * already provides. */
    ecs_id_record_t *idr = flecs_id_record_ensure(world, id);
    ecs_table_record_t *tr = (ecs_table_record_t*)flecs_id_record_get_table(
            idr, table);
    if (!tr) {
        tr = ecs_vec_append_t(&world->allocator, records, ecs_table_record_t);
        tr->column = column;
        tr->count = 1;

        ecs_table_cache_insert(&idr->cache, table, &tr->hdr);
    } else {
        tr->count ++;
    }

    ecs_assert(tr->hdr.cache != NULL, ECS_INTERNAL_ERROR, NULL);
}

/* Main table initialization function */
void flecs_table_init(
    ecs_world_t *world,
    ecs_table_t *table,
    ecs_table_t *from)
{
    /* Make sure table->flags is initialized */
    flecs_table_init_flags(world, table);

    /* The following code walks the table type to discover which id records the
     * table needs to register table records with. 
     *
     * In addition to registering itself with id records for each id in the
     * table type, a table also registers itself with wildcard id records. For
     * example, if a table contains (Eats, Apples), it will register itself with
     * wildcard id records (Eats, *),  (*, Apples) and (*, *). This makes it
     * easier for wildcard queries to find the relevant tables. */

    int32_t dst_i = 0, dst_count = table->type.count;
    int32_t src_i = 0, src_count = 0;
    ecs_id_t *dst_ids = table->type.array;
    ecs_id_t *src_ids = NULL;
    ecs_table_record_t *tr = NULL, *src_tr = NULL;
    if (from) {
        src_count = from->type.count;
        src_ids = from->type.array;
        src_tr = from->_->records;
    }

    /* We don't know in advance how large the records array will be, so use
     * cached vector. This eliminates unnecessary allocations, and/or expensive
     * iterations to determine how many records we need. */
    ecs_allocator_t *a = &world->allocator;
    ecs_vec_t *records = &world->store.records;
    ecs_vec_reset_t(a, records, ecs_table_record_t);
    ecs_id_record_t *idr, *childof_idr = NULL;

    int32_t last_id = -1; /* Track last regular (non-pair) id */
    int32_t first_pair = -1; /* Track the first pair in the table */
    int32_t first_role = -1; /* Track first id with role */

    /* Scan to find boundaries of regular ids, pairs and roles */
    for (dst_i = 0; dst_i < dst_count; dst_i ++) {
        ecs_id_t dst_id = dst_ids[dst_i];
        if (first_pair == -1 && ECS_IS_PAIR(dst_id)) {
            first_pair = dst_i;
        }
        if ((dst_id & ECS_COMPONENT_MASK) == dst_id) {
            last_id = dst_i;
        } else if (first_role == -1 && !ECS_IS_PAIR(dst_id)) {
            first_role = dst_i;
        }
    }

    /* The easy part: initialize a record for every id in the type */
    for (dst_i = 0; (dst_i < dst_count) && (src_i < src_count); ) {
        ecs_id_t dst_id = dst_ids[dst_i];
        ecs_id_t src_id = src_ids[src_i];

        idr = NULL;

        if (dst_id == src_id) {
            ecs_assert(src_tr != NULL, ECS_INTERNAL_ERROR, NULL);
            idr = (ecs_id_record_t*)src_tr[src_i].hdr.cache;
        } else if (dst_id < src_id) {
            idr = flecs_id_record_ensure(world, dst_id);
        }
        if (idr) {
            tr = ecs_vec_append_t(a, records, ecs_table_record_t);
            tr->hdr.cache = (ecs_table_cache_t*)idr;
            tr->column = dst_i;
            tr->count = 1;
        }

        dst_i += dst_id <= src_id;
        src_i += dst_id >= src_id;
    }

    /* Add remaining ids that the "from" table didn't have */
    for (; (dst_i < dst_count); dst_i ++) {
        ecs_id_t dst_id = dst_ids[dst_i];
        tr = ecs_vec_append_t(a, records, ecs_table_record_t);
        idr = flecs_id_record_ensure(world, dst_id);
        tr->hdr.cache = (ecs_table_cache_t*)idr;
        ecs_assert(tr->hdr.cache != NULL, ECS_INTERNAL_ERROR, NULL);
        tr->column = dst_i;
        tr->count = 1;
    }

    /* We're going to insert records from the vector into the index that
     * will get patched up later. To ensure the record pointers don't get
     * invalidated we need to grow the vector so that it won't realloc as
     * we're adding the next set of records */
    if (first_role != -1 || first_pair != -1) {
        int32_t start = first_role;
        if (first_pair != -1 && (start != -1 || first_pair < start)) {
            start = first_pair;
        }

        /* Total number of records can never be higher than
         * - number of regular (non-pair) ids +
         * - three records for pairs: (R,T), (R,*), (*,T)
         * - one wildcard (*), one any (_) and one pair wildcard (*,*) record
         * - one record for (ChildOf, 0)
         */
        int32_t flag_id_count = dst_count - start;
        int32_t record_count = start + 3 * flag_id_count + 3 + 1;
        ecs_vec_set_min_size_t(a, records, ecs_table_record_t, record_count);
    }

    /* Add records for ids with roles (used by cleanup logic) */
    if (first_role != -1) {
        for (dst_i = first_role; dst_i < dst_count; dst_i ++) {
            ecs_id_t id = dst_ids[dst_i];
            if (!ECS_IS_PAIR(id)) {
                ecs_entity_t first = 0;
                ecs_entity_t second = 0;
                if (ECS_HAS_ID_FLAG(id, PAIR)) {
                    first = ECS_PAIR_FIRST(id);
                    second = ECS_PAIR_SECOND(id);
                } else {
                    first = id & ECS_COMPONENT_MASK;
                }
                if (first) {
                    flecs_table_append_to_records(world, table, records, 
                        ecs_pair(EcsFlag, first), dst_i);
                }
                if (second) {
                    flecs_table_append_to_records(world, table, records, 
                        ecs_pair(EcsFlag, second), dst_i);
                }
            }
        }
    }

    int32_t last_pair = -1;
    bool has_childof = table->flags & EcsTableHasChildOf;
    if (first_pair != -1) {
        /* Add a (Relationship, *) record for each relationship. */
        ecs_entity_t r = 0;
        for (dst_i = first_pair; dst_i < dst_count; dst_i ++) {
            ecs_id_t dst_id = dst_ids[dst_i];
            if (!ECS_IS_PAIR(dst_id)) {
                break; /* no more pairs */
            }
            if (r != ECS_PAIR_FIRST(dst_id)) { /* New relationship, new record */
                tr = ecs_vec_get_t(records, ecs_table_record_t, dst_i);

                ecs_id_record_t *p_idr = (ecs_id_record_t*)tr->hdr.cache;
                r = ECS_PAIR_FIRST(dst_id);
                if (r == EcsChildOf) {
                    childof_idr = p_idr;
                }

                idr = p_idr->parent; /* (R, *) */
                ecs_assert(idr != NULL, ECS_INTERNAL_ERROR, NULL);

                tr = ecs_vec_append_t(a, records, ecs_table_record_t);
                tr->hdr.cache = (ecs_table_cache_t*)idr;
                tr->column = dst_i;
                tr->count = 0;
            }

            ecs_assert(tr != NULL, ECS_INTERNAL_ERROR, NULL);
            tr->count ++;
        }

        last_pair = dst_i;

        /* Add a (*, Target) record for each relationship target. Type
         * ids are sorted relationship-first, so we can't simply do a single linear 
         * scan to find all occurrences for a target. */
        for (dst_i = first_pair; dst_i < last_pair; dst_i ++) {
            ecs_id_t dst_id = dst_ids[dst_i];
            ecs_id_t tgt_id = ecs_pair(EcsWildcard, ECS_PAIR_SECOND(dst_id));

            flecs_table_append_to_records(
                world, table, records, tgt_id, dst_i);
        }
    }

    /* Lastly, add records for all-wildcard ids */
    if (last_id >= 0) {
        tr = ecs_vec_append_t(a, records, ecs_table_record_t);
        tr->hdr.cache = (ecs_table_cache_t*)world->idr_wildcard;
        tr->column = 0;
        tr->count = last_id + 1;
    }
    if (last_pair - first_pair) {
        tr = ecs_vec_append_t(a, records, ecs_table_record_t);
        tr->hdr.cache = (ecs_table_cache_t*)world->idr_wildcard_wildcard;
        tr->column = first_pair;
        tr->count = last_pair - first_pair;
    }
    if (dst_count) {
        tr = ecs_vec_append_t(a, records, ecs_table_record_t);
        tr->hdr.cache = (ecs_table_cache_t*)world->idr_any;
        tr->column = 0;
        tr->count = 1;
    }
    if (dst_count && !has_childof) {
        tr = ecs_vec_append_t(a, records, ecs_table_record_t);
        childof_idr = world->idr_childof_0;
        tr->hdr.cache = (ecs_table_cache_t*)childof_idr;
        tr->column = 0;
        tr->count = 1;
    }

    /* Now that all records have been added, copy them to array */
    int32_t i, dst_record_count = ecs_vec_count(records);
    ecs_table_record_t *dst_tr = flecs_wdup_n(world, ecs_table_record_t, 
        dst_record_count, ecs_vec_first_t(records, ecs_table_record_t));
    table->_->record_count = flecs_ito(uint16_t, dst_record_count);
    table->_->records = dst_tr;

    /* Register & patch up records */
    for (i = 0; i < dst_record_count; i ++) {
        tr = &dst_tr[i];
        idr = (ecs_id_record_t*)dst_tr[i].hdr.cache;
        ecs_assert(idr != NULL, ECS_INTERNAL_ERROR, NULL);

        if (ecs_table_cache_get(&idr->cache, table)) {
            /* If this is a target wildcard record it has already been 
             * registered, but the record is now at a different location in
             * memory. Patch up the linked list with the new address */
            ecs_table_cache_replace(&idr->cache, table, &tr->hdr);
        } else {
            /* Other records are not registered yet */
            ecs_assert(idr != NULL, ECS_INTERNAL_ERROR, NULL);
            ecs_table_cache_insert(&idr->cache, table, &tr->hdr);
        }

        /* Claim id record so it stays alive as long as the table exists */
        flecs_id_record_claim(world, idr);

        /* Initialize event flags */
        table->flags |= idr->flags & EcsIdEventMask;

        if (idr->flags & EcsIdAlwaysOverride) {
            table->flags |= EcsTableHasOverrides;
        }
    }

    flecs_table_init_storage_table(world, table);
    flecs_table_init_data(world, table);

    if (table->flags & EcsTableHasName) {
        ecs_assert(childof_idr != NULL, ECS_INTERNAL_ERROR, NULL);
        table->_->name_index = 
            flecs_id_record_name_index_ensure(world, childof_idr);
        ecs_assert(table->_->name_index != NULL, ECS_INTERNAL_ERROR, NULL);
    }

    if (table->flags & EcsTableHasOnTableCreate) {
        flecs_emit(world, world, &(ecs_event_desc_t) {
            .ids = &table->type,
            .event = EcsOnTableCreate,
            .table = table,
            .flags = EcsEventTableOnly,
            .observable = world
        });
    }
}

/* Unregister table from id records */
static
void flecs_table_records_unregister(
    ecs_world_t *world,
    ecs_table_t *table)
{
    uint64_t table_id = table->id;
    int32_t i, count = table->_->record_count;
    for (i = 0; i < count; i ++) {
        ecs_table_record_t *tr = &table->_->records[i];
        ecs_table_cache_t *cache = tr->hdr.cache;
        ecs_id_t id = ((ecs_id_record_t*)cache)->id;

        ecs_assert(tr->hdr.cache == cache, ECS_INTERNAL_ERROR, NULL);
        ecs_assert(tr->hdr.table == table, ECS_INTERNAL_ERROR, NULL);
        ecs_assert(flecs_id_record_get(world, id) == (ecs_id_record_t*)cache,
            ECS_INTERNAL_ERROR, NULL);
        (void)id;

        ecs_table_cache_remove(cache, table_id, &tr->hdr);
        flecs_id_record_release(world, (ecs_id_record_t*)cache);
    }

    flecs_wfree_n(world, ecs_table_record_t, count, table->_->records);
}

/* Keep track for what kind of builtin events observers are registered that can
 * potentially match the table. This allows code to early out of calling the
 * emit function that notifies observers. */
static
void flecs_table_add_trigger_flags(
    ecs_world_t *world, 
    ecs_table_t *table, 
    ecs_entity_t event) 
{
    (void)world;

    if (event == EcsOnAdd) {
        table->flags |= EcsTableHasOnAdd;
    } else if (event == EcsOnRemove) {
        table->flags |= EcsTableHasOnRemove;
    } else if (event == EcsOnSet) {
        table->flags |= EcsTableHasOnSet;
    } else if (event == EcsUnSet) {
        table->flags |= EcsTableHasUnSet;
    } else if (event == EcsOnTableFill) {
        table->flags |= EcsTableHasOnTableFill;
    } else if (event == EcsOnTableEmpty) {
        table->flags |= EcsTableHasOnTableEmpty;
    }
}

/* Invoke OnRemove observers for all entities in table. Useful during table 
 * deletion or when clearing entities from a table. */
static
void flecs_table_notify_on_remove(
    ecs_world_t *world,
    ecs_table_t *table,
    ecs_data_t *data)
{
    int32_t count = data->entities.count;
    if (count) {
        flecs_notify_on_remove(world, table, NULL, 0, count, &table->type);
    }
}

/* Invoke type hook for entities in table */
static
void flecs_table_invoke_hook(
    ecs_world_t *world,
    ecs_table_t *table,
    ecs_iter_action_t callback,
    ecs_entity_t event,
    ecs_vec_t *column,
    ecs_entity_t *entities,
    ecs_id_t id,
    int32_t row,
    int32_t count,
    ecs_type_info_t *ti)
{
    ecs_assert(ti != NULL, ECS_INTERNAL_ERROR, NULL);
    void *ptr = ecs_vec_get(column, ti->size, row);
    flecs_invoke_hook(
        world, table, count, row, entities, ptr, id, ti, event, callback);
}

/* Construct components */
static
void flecs_table_invoke_ctor(
    ecs_type_info_t *ti,
    ecs_vec_t *column,
    int32_t row,
    int32_t count)
{
    ecs_assert(ti != NULL, ECS_INTERNAL_ERROR, NULL);
    ecs_xtor_t ctor = ti->hooks.ctor;
    if (ctor) {
        void *ptr = ecs_vec_get(column, ti->size, row);
        ctor(ptr, count, ti);
    }
}

/* Destruct components */
static
void flecs_table_invoke_dtor(
    ecs_type_info_t *ti,
    ecs_vec_t *column,
    int32_t row,
    int32_t count)
{
    ecs_assert(ti != NULL, ECS_INTERNAL_ERROR, NULL);

    ecs_xtor_t dtor = ti->hooks.dtor;
    if (dtor) {
        void *ptr = ecs_vec_get(column, ti->size, row);
        dtor(ptr, count, ti);
    }
}

/* Run hooks that get invoked when component is added to entity */
static
void flecs_table_invoke_add_hooks(
    ecs_world_t *world,
    ecs_table_t *table,
    ecs_type_info_t *ti,
    ecs_vec_t *column,
    ecs_entity_t *entities,
    ecs_id_t id,
    int32_t row,
    int32_t count,
    bool construct)
{
    ecs_assert(ti != NULL, ECS_INTERNAL_ERROR, NULL);

    if (construct) {
        flecs_table_invoke_ctor(ti, column, row, count);
    }

    ecs_iter_action_t on_add = ti->hooks.on_add;
    if (on_add) {
        flecs_table_invoke_hook(world, table, on_add, EcsOnAdd, column,
            entities, id, row, count, ti);
    }
}

/* Run hooks that get invoked when component is removed from entity */
static
void flecs_table_invoke_remove_hooks(
    ecs_world_t *world,
    ecs_table_t *table,
    ecs_type_info_t *ti,
    ecs_vec_t *column,
    ecs_entity_t *entities,
    ecs_id_t id,
    int32_t row,
    int32_t count,
    bool dtor)
{
    ecs_assert(ti != NULL, ECS_INTERNAL_ERROR, NULL);

    ecs_iter_action_t on_remove = ti->hooks.on_remove;
    if (on_remove) {
        flecs_table_invoke_hook(world, table, on_remove, EcsOnRemove, column,
            entities, id, row, count, ti);
    }
    
    if (dtor) {
        flecs_table_invoke_dtor(ti, column, row, count);
    }
}

/* Destruct all components and/or delete all entities in table in range */
static
void flecs_table_dtor_all(
    ecs_world_t *world,
    ecs_table_t *table,
    ecs_data_t *data,
    int32_t row,
    int32_t count,
    bool update_entity_index,
    bool is_delete)
{
    /* Can't delete and not update the entity index */
    ecs_assert(!is_delete || update_entity_index, ECS_INTERNAL_ERROR, NULL);

    ecs_id_t *ids = table->storage_ids;
    int32_t ids_count = table->storage_count;
    ecs_record_t **records = data->records.array;
    ecs_entity_t *entities = data->entities.array;
    int32_t i, c, end = row + count;

    (void)records;

    if (is_delete && table->_->traversable_count) {
        /* If table contains monitored entities with traversable relationships,
         * make sure to invalidate observer cache */
        flecs_emit_propagate_invalidate(world, table, row, count);
    }

    /* If table has components with destructors, iterate component columns */
    if (table->flags & EcsTableHasDtors) {
        /* Throw up a lock just to be sure */
        table->_->lock = true;

        /* Run on_remove callbacks first before destructing components */
        for (c = 0; c < ids_count; c++) {
            ecs_vec_t *column = &data->columns[c];
            ecs_type_info_t *ti = table->type_info[c];
            ecs_iter_action_t on_remove = ti->hooks.on_remove;
            if (on_remove) {
                flecs_table_invoke_hook(world, table, on_remove, EcsOnRemove, 
                    column, &entities[row], ids[c], row, count, ti);
            }
        }

        /* Destruct components */
        for (c = 0; c < ids_count; c++) {
            flecs_table_invoke_dtor(table->type_info[c], &data->columns[c], 
                row, count);
        }

        /* Iterate entities first, then components. This ensures that only one
         * entity is invalidated at a time, which ensures that destructors can
         * safely access other entities. */
        for (i = row; i < end; i ++) {
            /* Update entity index after invoking destructors so that entity can
             * be safely used in destructor callbacks. */
            if (update_entity_index) {
                ecs_entity_t e = entities[i];
                ecs_assert(!e || ecs_is_valid(world, e), 
                    ECS_INTERNAL_ERROR, NULL);
                ecs_assert(!e || records[i] == flecs_entities_get(world, e), 
                    ECS_INTERNAL_ERROR, NULL);
                ecs_assert(!e || records[i]->table == table, 
                    ECS_INTERNAL_ERROR, NULL);

                if (is_delete) {
                    flecs_entities_remove(world, e);
                    ecs_assert(ecs_is_valid(world, e) == false, 
                        ECS_INTERNAL_ERROR, NULL);
                } else {
                    // If this is not a delete, clear the entity index record
                    records[i]->table = NULL;
                    records[i]->row = 0;
                }
            } else {
                /* This should only happen in rare cases, such as when the data
                 * cleaned up is not part of the world (like with snapshots) */
            }
        }

        table->_->lock = false;

    /* If table does not have destructors, just update entity index */
    } else if (update_entity_index) {
        if (is_delete) {
            for (i = row; i < end; i ++) {
                ecs_entity_t e = entities[i];
                ecs_assert(!e || ecs_is_valid(world, e), ECS_INTERNAL_ERROR, NULL);
                ecs_assert(!e || records[i] == flecs_entities_get(world, e), 
                    ECS_INTERNAL_ERROR, NULL);
                ecs_assert(!e || records[i]->table == table, 
                    ECS_INTERNAL_ERROR, NULL);

                flecs_entities_remove(world, e);
                ecs_assert(!ecs_is_valid(world, e), ECS_INTERNAL_ERROR, NULL);
            } 
        } else {
            for (i = row; i < end; i ++) {
                ecs_entity_t e = entities[i];
                ecs_assert(!e || ecs_is_valid(world, e), ECS_INTERNAL_ERROR, NULL);
                ecs_assert(!e || records[i] == flecs_entities_get(world, e), 
                    ECS_INTERNAL_ERROR, NULL);
                ecs_assert(!e || records[i]->table == table, 
                    ECS_INTERNAL_ERROR, NULL);
                records[i]->table = NULL;
                records[i]->row = records[i]->row & ECS_ROW_FLAGS_MASK;
                (void)e;
            }
        }      
    }
}

/* Cleanup table storage */
static
void flecs_table_fini_data(
    ecs_world_t *world,
    ecs_table_t *table,
    ecs_data_t *data,
    bool do_on_remove,
    bool update_entity_index,
    bool is_delete,
    bool deactivate)
{
    ecs_assert(!table->_->lock, ECS_LOCKED_STORAGE, NULL);

    if (!data) {
        return;
    }

    if (do_on_remove) {
        flecs_table_notify_on_remove(world, table, data);        
    }

    int32_t count = flecs_table_data_count(data);
    if (count) {
        flecs_table_dtor_all(world, table, data, 0, count, 
            update_entity_index, is_delete);
    }

    /* Sanity check */
    ecs_assert(data->records.count == 
        data->entities.count, ECS_INTERNAL_ERROR, NULL);

    ecs_vec_t *columns = data->columns;
    if (columns) {
        int32_t c, column_count = table->storage_count;
        for (c = 0; c < column_count; c ++) {
            /* Sanity check */
            ecs_assert(columns[c].count == data->entities.count,
                ECS_INTERNAL_ERROR, NULL);
            ecs_vec_fini(&world->allocator, 
                &columns[c], table->type_info[c]->size);
        }
        flecs_wfree_n(world, ecs_vec_t, column_count, columns);
        data->columns = NULL;
    }

    ecs_table__t *meta = table->_;
    ecs_switch_t *sw_columns = meta->sw_columns;
    if (sw_columns) {
        int32_t c, column_count = meta->sw_count;
        for (c = 0; c < column_count; c ++) {
            flecs_switch_fini(&sw_columns[c]);
        }
        flecs_wfree_n(world, ecs_switch_t, column_count, sw_columns);
        meta->sw_columns = NULL;
    }

    ecs_bitset_t *bs_columns = meta->bs_columns;
    if (bs_columns) {
        int32_t c, column_count = meta->bs_count;
        for (c = 0; c < column_count; c ++) {
            flecs_bitset_fini(&bs_columns[c]);
        }
        flecs_wfree_n(world, ecs_bitset_t, column_count, bs_columns);
        meta->bs_columns = NULL;
    }

    ecs_vec_fini_t(&world->allocator, &data->entities, ecs_entity_t);
    ecs_vec_fini_t(&world->allocator, &data->records, ecs_record_t*);

    if (deactivate && count) {
        flecs_table_set_empty(world, table);
    }

    table->_->traversable_count = 0;
    table->flags &= ~EcsTableHasTraversable;
}

/* Cleanup, no OnRemove, don't update entity index, don't deactivate table */
void flecs_table_clear_data(
    ecs_world_t *world,
    ecs_table_t *table,
    ecs_data_t *data)
{
    flecs_table_fini_data(world, table, data, false, false, false, false);
}

/* Cleanup, no OnRemove, clear entity index, deactivate table */
void flecs_table_clear_entities_silent(
    ecs_world_t *world,
    ecs_table_t *table)
{
    flecs_table_fini_data(world, table, &table->data, false, true, false, true);
}

/* Cleanup, run OnRemove, clear entity index, deactivate table */
void flecs_table_clear_entities(
    ecs_world_t *world,
    ecs_table_t *table)
{
    flecs_table_fini_data(world, table, &table->data, true, true, false, true);
}

/* Cleanup, run OnRemove, delete from entity index, deactivate table */
void flecs_table_delete_entities(
    ecs_world_t *world,
    ecs_table_t *table)
{
    flecs_table_fini_data(world, table, &table->data, true, true, true, true);
}

/* Unset all components in table. This function is called before a table is 
 * deleted, and invokes all UnSet handlers, if any */
void flecs_table_remove_actions(
    ecs_world_t *world,
    ecs_table_t *table)
{
    (void)world;
    flecs_table_notify_on_remove(world, table, &table->data);
}

/* Free table resources. */
void flecs_table_free(
    ecs_world_t *world,
    ecs_table_t *table)
{
    bool is_root = table == &world->store.root;
    ecs_assert(!table->_->lock, ECS_LOCKED_STORAGE, NULL);
    ecs_assert(is_root || table->id != 0, ECS_INTERNAL_ERROR, NULL);
    ecs_assert(is_root || flecs_sparse_is_alive(&world->store.tables, table->id),
        ECS_INTERNAL_ERROR, NULL);
    (void)world;

    ecs_assert(table->_->refcount == 0, ECS_INTERNAL_ERROR, NULL);

    if (!is_root && !(world->flags & EcsWorldQuit)) {
        if (table->flags & EcsTableHasOnTableDelete) {
            flecs_emit(world, world, &(ecs_event_desc_t) {
                .ids = &table->type,
                .event = EcsOnTableDelete,
                .table = table,
                .flags = EcsEventTableOnly,
                .observable = world
            });
        }
    }

    if (ecs_should_log_2()) {
        char *expr = ecs_type_str(world, &table->type);
        ecs_dbg_2(
            "#[green]table#[normal] [%s] #[red]deleted#[reset] with id %d", 
            expr, table->id);
        ecs_os_free(expr);
        ecs_log_push_2();
    }

    world->info.empty_table_count -= (ecs_table_count(table) == 0);

    /* Cleanup data, no OnRemove, delete from entity index, don't deactivate */
    flecs_table_fini_data(world, table, &table->data, false, true, true, false);
    flecs_table_clear_edges(world, table);

    if (!is_root) {
        ecs_type_t ids = {
            .array = table->type.array,
            .count = table->type.count
        };

        flecs_hashmap_remove_w_hash(
            &world->store.table_map, &ids, ecs_table_t*, table->_->hash);
    }

    flecs_wfree_n(world, int32_t, table->storage_count + 1, table->dirty_state);
    flecs_wfree_n(world, int32_t, table->storage_count + table->type.count, 
        table->storage_map);
    flecs_table_records_unregister(world, table);

    ecs_table_t *storage_table = table->storage_table;
    if (storage_table == table) {
        if (table->type_info) {
            flecs_wfree_n(world, ecs_type_info_t*, table->storage_count, 
                table->type_info);
        }
    } else if (storage_table) {
        flecs_table_release(world, storage_table);
    }

    /* Update counters */
    world->info.table_count --;
    world->info.table_record_count -= table->_->record_count;
    world->info.table_storage_count -= table->storage_count;
    world->info.table_delete_total ++;

    if (!table->storage_count) {
        world->info.tag_table_count --;
    } else {
        world->info.trivial_table_count -= !(table->flags & EcsTableIsComplex);
    }

    flecs_free_t(&world->allocator, ecs_table__t, table->_);

    if (!(world->flags & EcsWorldFini)) {
        ecs_assert(!is_root, ECS_INTERNAL_ERROR, NULL);
        flecs_table_free_type(world, table);
        flecs_sparse_remove_t(&world->store.tables, ecs_table_t, table->id);
    }

    ecs_log_pop_2();
}

/* Increase refcount of table. A table will not be freed until its refcount
 * reaches zero. Refcounting is primarily used to prevent storage tables from
 * being freed while they are still being referred to. 
 * Tables do not form cyclical dependencies. */
void flecs_table_claim(
    ecs_world_t *world,
    ecs_table_t *table)
{
    ecs_poly_assert(world, ecs_world_t);
    ecs_assert(table != NULL, ECS_INTERNAL_ERROR, NULL);
    ecs_assert(table->_->refcount > 0, ECS_INTERNAL_ERROR, NULL);
    table->_->refcount ++;
    (void)world;
}

/* Decrease refcount of table */
bool flecs_table_release(
    ecs_world_t *world,
    ecs_table_t *table)
{
    ecs_poly_assert(world, ecs_world_t);
    ecs_assert(table != NULL, ECS_INTERNAL_ERROR, NULL);
    ecs_assert(table->_->refcount > 0, ECS_INTERNAL_ERROR, NULL);

    if (--table->_->refcount == 0) {
        flecs_table_free(world, table);
        return true;
    }
    
    return false;
}

/* Free table type. Do this separately from freeing the table as types can be
 * in use by application destructors. */
void flecs_table_free_type(
    ecs_world_t *world,
    ecs_table_t *table)
{
    flecs_wfree_n(world, ecs_id_t, table->type.count, table->type.array);
}

/* Reset a table to its initial state. */
void flecs_table_reset(
    ecs_world_t *world,
    ecs_table_t *table)
{
    ecs_assert(!table->_->lock, ECS_LOCKED_STORAGE, NULL);
    flecs_table_clear_edges(world, table);
}

/* Keep track of number of traversable entities in table. A traversable entity
 * is an entity used as target in a pair with a traversable relationship. The
 * traversable count and flag are used by code to early out of mechanisms like
 * event propagation and recursive cleanup. */
void flecs_table_traversable_add(
    ecs_table_t *table,
    int32_t value)
{
    int32_t result = table->_->traversable_count += value;
    ecs_assert(result >= 0, ECS_INTERNAL_ERROR, NULL);
    if (result == 0) {
        table->flags &= ~EcsTableHasTraversable;
    } else if (result == value) {
        table->flags |= EcsTableHasTraversable;
    }
}

/* Mark table column dirty. This usually happens as the result of a set 
 * operation, or iteration of a query with [out] fields. */
static
void flecs_table_mark_table_dirty(
    ecs_world_t *world,
    ecs_table_t *table,
    int32_t index)
{
    (void)world;
    if (table->dirty_state) {
        table->dirty_state[index] ++;
    }
}

/* Mark table component dirty */
void flecs_table_mark_dirty(
    ecs_world_t *world,
    ecs_table_t *table,
    ecs_entity_t component)
{
    ecs_assert(!table->_->lock, ECS_LOCKED_STORAGE, NULL);
    ecs_assert(table != NULL, ECS_INTERNAL_ERROR, NULL);

    if (table->dirty_state) {
        int32_t index = ecs_search(world, table->storage_table, component, 0);
        ecs_assert(index != -1, ECS_INTERNAL_ERROR, NULL);
        table->dirty_state[index + 1] ++;
    }
}

/* Get (or create) dirty state of table. Used by queries for change tracking */
int32_t* flecs_table_get_dirty_state(
    ecs_world_t *world,
    ecs_table_t *table)
{
    ecs_poly_assert(world, ecs_world_t);
    ecs_assert(table != NULL, ECS_INTERNAL_ERROR, NULL);
    if (!table->dirty_state) {
        int32_t column_count = table->storage_count;
        table->dirty_state = flecs_alloc_n(&world->allocator,
             int32_t, column_count + 1);
        ecs_assert(table->dirty_state != NULL, ECS_INTERNAL_ERROR, NULL);
        for (int i = 0; i < column_count + 1; i ++) {
            table->dirty_state[i] = 1;
        }
    }
    return table->dirty_state;
}

/* Table move logic for switch (union relationship) column */
static
void flecs_table_move_switch_columns(
    ecs_table_t *dst_table, 
    int32_t dst_index,
    ecs_table_t *src_table, 
    int32_t src_index,
    int32_t count,
    bool clear)
{
    ecs_table__t *dst_meta = dst_table->_;
    ecs_table__t *src_meta = src_table->_;
    if (!dst_meta && !src_meta) {
        return;
    }

    int32_t i_old = 0, src_column_count = src_meta ? src_meta->sw_count : 0;
    int32_t i_new = 0, dst_column_count = dst_meta ? dst_meta->sw_count : 0;
    if (!src_column_count && !dst_column_count) {
        return;
    }

    ecs_switch_t *src_columns = src_meta ? src_meta->sw_columns : NULL;
    ecs_switch_t *dst_columns = dst_meta ? dst_meta->sw_columns : NULL;

    ecs_type_t dst_type = dst_table->type;
    ecs_type_t src_type = src_table->type;

    int32_t offset_new = dst_meta ? dst_meta->sw_offset : 0;
    int32_t offset_old = src_meta ? src_meta->sw_offset : 0;

    ecs_id_t *dst_ids = dst_type.array;
    ecs_id_t *src_ids = src_type.array;

    for (; (i_new < dst_column_count) && (i_old < src_column_count);) {
        ecs_entity_t dst_id = dst_ids[i_new + offset_new];
        ecs_entity_t src_id = src_ids[i_old + offset_old];

        if (dst_id == src_id) {
            ecs_switch_t *src_switch = &src_columns[i_old];
            ecs_switch_t *dst_switch = &dst_columns[i_new];

            flecs_switch_ensure(dst_switch, dst_index + count);

            int i;
            for (i = 0; i < count; i ++) {
                uint64_t value = flecs_switch_get(src_switch, src_index + i);
                flecs_switch_set(dst_switch, dst_index + i, value);
            }

            if (clear) {
                ecs_assert(count == flecs_switch_count(src_switch), 
                    ECS_INTERNAL_ERROR, NULL);
                flecs_switch_clear(src_switch);
            }
        } else if (dst_id > src_id) {
            ecs_switch_t *src_switch = &src_columns[i_old];
            flecs_switch_clear(src_switch);
        }

        i_new += dst_id <= src_id;
        i_old += dst_id >= src_id;
    }

    /* Clear remaining columns */
    if (clear) {
        for (; (i_old < src_column_count); i_old ++) {
            ecs_switch_t *src_switch = &src_columns[i_old];
            ecs_assert(count == flecs_switch_count(src_switch), 
                ECS_INTERNAL_ERROR, NULL);
            flecs_switch_clear(src_switch);
        }
    }
}

/* Table move logic for bitset (toggle component) column */
static
void flecs_table_move_bitset_columns(
    ecs_table_t *dst_table, 
    int32_t dst_index,
    ecs_table_t *src_table, 
    int32_t src_index,
    int32_t count,
    bool clear)
{
    ecs_table__t *dst_meta = dst_table->_;
    ecs_table__t *src_meta = src_table->_;
    if (!dst_meta && !src_meta) {
        return;
    }

    int32_t i_old = 0, src_column_count = src_meta ? src_meta->bs_count : 0;
    int32_t i_new = 0, dst_column_count = dst_meta ? dst_meta->bs_count : 0;

    if (!src_column_count && !dst_column_count) {
        return;
    }

    ecs_bitset_t *src_columns = src_meta ? src_meta->bs_columns : NULL;
    ecs_bitset_t *dst_columns = dst_meta ? dst_meta->bs_columns : NULL;

    ecs_type_t dst_type = dst_table->type;
    ecs_type_t src_type = src_table->type;

    int32_t offset_new = dst_meta ? dst_meta->bs_offset : 0;
    int32_t offset_old = src_meta ? src_meta->bs_offset : 0;

    ecs_id_t *dst_ids = dst_type.array;
    ecs_id_t *src_ids = src_type.array;

    for (; (i_new < dst_column_count) && (i_old < src_column_count);) {
        ecs_id_t dst_id = dst_ids[i_new + offset_new];
        ecs_id_t src_id = src_ids[i_old + offset_old];

        if (dst_id == src_id) {
            ecs_bitset_t *src_bs = &src_columns[i_old];
            ecs_bitset_t *dst_bs = &dst_columns[i_new];

            flecs_bitset_ensure(dst_bs, dst_index + count);

            int i;
            for (i = 0; i < count; i ++) {
                uint64_t value = flecs_bitset_get(src_bs, src_index + i);
                flecs_bitset_set(dst_bs, dst_index + i, value);
            }

            if (clear) {
                ecs_assert(count == flecs_bitset_count(src_bs), 
                    ECS_INTERNAL_ERROR, NULL);
                flecs_bitset_fini(src_bs);
            }
        } else if (dst_id > src_id) {
            ecs_bitset_t *src_bs = &src_columns[i_old];
            flecs_bitset_fini(src_bs);
        }

        i_new += dst_id <= src_id;
        i_old += dst_id >= src_id;
    }

    /* Clear remaining columns */
    if (clear) {
        for (; (i_old < src_column_count); i_old ++) {
            ecs_bitset_t *src_bs = &src_columns[i_old];
            ecs_assert(count == flecs_bitset_count(src_bs), 
                ECS_INTERNAL_ERROR, NULL);
            flecs_bitset_fini(src_bs);
        }
    }
}

/* Grow table column. When a column needs to be reallocated this function takes
 * care of correctly invoking ctor/move/dtor hooks. */
static
void* flecs_table_grow_column(
    ecs_world_t *world,
    ecs_vec_t *column,
    ecs_type_info_t *ti,
    int32_t to_add,
    int32_t dst_size,
    bool construct)
{
    ecs_assert(column != NULL, ECS_INTERNAL_ERROR, NULL);
    ecs_assert(ti != NULL, ECS_INTERNAL_ERROR, NULL);

    int32_t size = ti->size;
    int32_t count = column->count;
    int32_t src_size = column->size;
    int32_t dst_count = count + to_add;
    bool can_realloc = dst_size != src_size;
    void *result = NULL;

    ecs_assert(dst_size >= dst_count, ECS_INTERNAL_ERROR, NULL);

    /* If the array could possibly realloc and the component has a move action 
     * defined, move old elements manually */
    ecs_move_t move_ctor;
    if (count && can_realloc && (move_ctor = ti->hooks.ctor_move_dtor)) {
        ecs_xtor_t ctor = ti->hooks.ctor;
        ecs_assert(ctor != NULL, ECS_INTERNAL_ERROR, NULL);
        ecs_assert(move_ctor != NULL, ECS_INTERNAL_ERROR, NULL);

        /* Create  vector */
        ecs_vec_t dst;
        ecs_vec_init(&world->allocator, &dst, size, dst_size);
        dst.count = dst_count;

        void *src_buffer = column->array;
        void *dst_buffer = dst.array;

        /* Move (and construct) existing elements to new vector */
        move_ctor(dst_buffer, src_buffer, count, ti);

        if (construct) {
            /* Construct new element(s) */
            result = ECS_ELEM(dst_buffer, size, count);
            ctor(result, to_add, ti);
        }

        /* Free old vector */
        ecs_vec_fini(&world->allocator, column, ti->size);

        *column = dst;
    } else {
        /* If array won't realloc or has no move, simply add new elements */
        if (can_realloc) {
            ecs_vec_set_size(&world->allocator, column, size, dst_size);
        }

        result = ecs_vec_grow(&world->allocator, column, size, to_add);

        ecs_xtor_t ctor;
        if (construct && (ctor = ti->hooks.ctor)) {
            /* If new elements need to be constructed and component has a
             * constructor, construct */
            ctor(result, to_add, ti);
        }
    }

    ecs_assert(column->size == dst_size, ECS_INTERNAL_ERROR, NULL);

    return result;
}

/* Grow all data structures in a table */
static
int32_t flecs_table_grow_data(
    ecs_world_t *world,
    ecs_table_t *table,
    ecs_data_t *data,
    int32_t to_add,
    int32_t size,
    const ecs_entity_t *ids)
{
    ecs_assert(table != NULL, ECS_INTERNAL_ERROR, NULL);
    ecs_assert(data != NULL, ECS_INTERNAL_ERROR, NULL);

    int32_t cur_count = flecs_table_data_count(data);
    int32_t column_count = table->storage_count;

    /* Add record to record ptr array */
    ecs_vec_set_size_t(&world->allocator, &data->records, ecs_record_t*, size);
    ecs_record_t **r = ecs_vec_last_t(&data->records, ecs_record_t*) + 1;
    data->records.count += to_add;
    if (data->records.size > size) {
        size = data->records.size;
    }

    /* Add entity to column with entity ids */
    ecs_vec_set_size_t(&world->allocator, &data->entities, ecs_entity_t, size);
    ecs_entity_t *e = ecs_vec_last_t(&data->entities, ecs_entity_t) + 1;
    data->entities.count += to_add;
    ecs_assert(data->entities.size == size, ECS_INTERNAL_ERROR, NULL);

    /* Initialize entity ids and record ptrs */
    int32_t i;
    if (ids) {
        ecs_os_memcpy_n(e, ids, ecs_entity_t, to_add);
    } else {
        ecs_os_memset(e, 0, ECS_SIZEOF(ecs_entity_t) * to_add);
    }
    ecs_os_memset(r, 0, ECS_SIZEOF(ecs_record_t*) * to_add);

    /* Add elements to each column array */
    ecs_vec_t *columns = data->columns;
    ecs_type_info_t **type_info = table->type_info;
    for (i = 0; i < column_count; i ++) {
        ecs_vec_t *column = &columns[i];
        ecs_type_info_t *ti = type_info[i];
        flecs_table_grow_column(world, column, ti, to_add, size, true);
        ecs_assert(columns[i].size == size, ECS_INTERNAL_ERROR, NULL);
        flecs_table_invoke_add_hooks(world, table, ti, column, e, table->type.array[i], 
            cur_count, to_add, false);
    }

    ecs_table__t *meta = table->_;
    int32_t sw_count = meta->sw_count;
    int32_t bs_count = meta->bs_count;
    ecs_switch_t *sw_columns = meta->sw_columns;
    ecs_bitset_t *bs_columns = meta->bs_columns; 

    /* Add elements to each switch column */
    for (i = 0; i < sw_count; i ++) {
        ecs_switch_t *sw = &sw_columns[i];
        flecs_switch_addn(sw, to_add);
    }

    /* Add elements to each bitset column */
    for (i = 0; i < bs_count; i ++) {
        ecs_bitset_t *bs = &bs_columns[i];
        flecs_bitset_addn(bs, to_add);
    }

    /* If the table is monitored indicate that there has been a change */
    flecs_table_mark_table_dirty(world, table, 0);

    if (!(world->flags & EcsWorldReadonly) && !cur_count) {
        flecs_table_set_empty(world, table);
    }

    /* Return index of first added entity */
    return cur_count;
}

/* Append operation for tables that don't have any complex logic */
static
void flecs_table_fast_append(
    ecs_world_t *world,
    ecs_type_info_t **type_info,
    ecs_vec_t *columns,
    int32_t count)
{
    /* Add elements to each column array */
    int32_t i;
    for (i = 0; i < count; i ++) {
        ecs_type_info_t *ti = type_info[i];
        ecs_vec_t *column = &columns[i];
        ecs_vec_append(&world->allocator, column, ti->size);
    }
}

/* Append entity to table */
int32_t flecs_table_append(
    ecs_world_t *world,
    ecs_table_t *table,
    ecs_entity_t entity,
    ecs_record_t *record,
    bool construct,
    bool on_add)
{
    ecs_assert(table != NULL, ECS_INTERNAL_ERROR, NULL);
    ecs_assert(!table->_->lock, ECS_LOCKED_STORAGE, NULL);
    ecs_assert(!(table->flags & EcsTableHasTarget), 
        ECS_INVALID_OPERATION, NULL);

    flecs_table_check_sanity(table);

    /* Get count & size before growing entities array. This tells us whether the
     * arrays will realloc */
    ecs_data_t *data = &table->data;
    int32_t count = data->entities.count;
    int32_t column_count = table->storage_count;
    ecs_vec_t *columns = table->data.columns;

    /* Grow buffer with entity ids, set new element to new entity */
    ecs_entity_t *e = ecs_vec_append_t(&world->allocator, 
        &data->entities, ecs_entity_t);
    ecs_assert(e != NULL, ECS_INTERNAL_ERROR, NULL);
    *e = entity;

    /* Add record ptr to array with record ptrs */
    ecs_record_t **r = ecs_vec_append_t(&world->allocator, 
        &data->records, ecs_record_t*);
    ecs_assert(r != NULL, ECS_INTERNAL_ERROR, NULL);
    *r = record;
 
    /* If the table is monitored indicate that there has been a change */
    flecs_table_mark_table_dirty(world, table, 0);
    ecs_assert(count >= 0, ECS_INTERNAL_ERROR, NULL);

    ecs_type_info_t **type_info = table->type_info;

    /* Fast path: no switch columns, no lifecycle actions */
    if (!(table->flags & EcsTableIsComplex)) {
        flecs_table_fast_append(world, type_info, columns, column_count);
        if (!count) {
            flecs_table_set_empty(world, table); /* See below */
        }
        return count;
    }

    ecs_entity_t *entities = data->entities.array;

    /* Reobtain size to ensure that the columns have the same size as the 
     * entities and record vectors. This keeps reasoning about when allocations
     * occur easier. */
    int32_t size = data->entities.size;

    /* Grow component arrays with 1 element */
    int32_t i;
    for (i = 0; i < column_count; i ++) {
        ecs_vec_t *column = &columns[i];
        ecs_type_info_t *ti = type_info[i];  
        flecs_table_grow_column(world, column, ti, 1, size, construct);

        ecs_iter_action_t on_add_hook;
        if (on_add && (on_add_hook = ti->hooks.on_add)) {
            flecs_table_invoke_hook(world, table, on_add_hook, EcsOnAdd, column,
                &entities[count], table->storage_ids[i], count, 1, ti);
        }

        ecs_assert(columns[i].size == 
            data->entities.size, ECS_INTERNAL_ERROR, NULL); 
        ecs_assert(columns[i].count == 
            data->entities.count, ECS_INTERNAL_ERROR, NULL);
    }

    ecs_table__t *meta = table->_;
    int32_t sw_count = meta->sw_count;
    int32_t bs_count = meta->bs_count;
    ecs_switch_t *sw_columns = meta->sw_columns;
    ecs_bitset_t *bs_columns = meta->bs_columns;

    /* Add element to each switch column */
    for (i = 0; i < sw_count; i ++) {
        ecs_assert(sw_columns != NULL, ECS_INTERNAL_ERROR, NULL);
        ecs_switch_t *sw = &sw_columns[i];
        flecs_switch_add(sw);
    }

    /* Add element to each bitset column */
    for (i = 0; i < bs_count; i ++) {
        ecs_assert(bs_columns != NULL, ECS_INTERNAL_ERROR, NULL);
        ecs_bitset_t *bs = &bs_columns[i];
        flecs_bitset_addn(bs, 1);
    }

    /* If this is the first entity in this table, signal queries so that the
     * table moves from an inactive table to an active table. */
    if (!count) {
        flecs_table_set_empty(world, table);
    }

    flecs_table_check_sanity(table);

    return count;
}

/* Delete last operation for tables that don't have any complex logic */
static
void flecs_table_fast_delete_last(
    ecs_vec_t *columns,
    int32_t column_count) 
{
    int i;
    for (i = 0; i < column_count; i ++) {
        ecs_vec_remove_last(&columns[i]);
    }
}

/* Delete operation for tables that don't have any complex logic */
static
void flecs_table_fast_delete(
    ecs_type_info_t **type_info,
    ecs_vec_t *columns,
    int32_t column_count,
    int32_t index) 
{
    int i;
    for (i = 0; i < column_count; i ++) {
        ecs_type_info_t *ti = type_info[i];
        ecs_vec_t *column = &columns[i];
        ecs_vec_remove(column, ti->size, index);
    }
}

/* Delete entity from table */
void flecs_table_delete(
    ecs_world_t *world,
    ecs_table_t *table,
    int32_t index,
    bool destruct)
{
    ecs_assert(world != NULL, ECS_INTERNAL_ERROR, NULL);
    ecs_assert(table != NULL, ECS_INTERNAL_ERROR, NULL);
    ecs_assert(!table->_->lock, ECS_LOCKED_STORAGE, NULL);
    ecs_assert(!(table->flags & EcsTableHasTarget), 
        ECS_INVALID_OPERATION, NULL);

    flecs_table_check_sanity(table);

    ecs_data_t *data = &table->data;
    int32_t count = data->entities.count;

    ecs_assert(count > 0, ECS_INTERNAL_ERROR, NULL);
    count --;
    ecs_assert(index <= count, ECS_INTERNAL_ERROR, NULL);

    /* Move last entity id to index */
    ecs_entity_t *entities = data->entities.array;
    ecs_entity_t entity_to_move = entities[count];
    ecs_entity_t entity_to_delete = entities[index];
    entities[index] = entity_to_move;
    ecs_vec_remove_last(&data->entities);

    /* Move last record ptr to index */
    ecs_assert(count < data->records.count, ECS_INTERNAL_ERROR, NULL);

    ecs_record_t **records = data->records.array;
    ecs_record_t *record_to_move = records[count];
    records[index] = record_to_move;
    ecs_vec_remove_last(&data->records); 

    /* Update record of moved entity in entity index */
    if (index != count) {
        if (record_to_move) {
            uint32_t row_flags = record_to_move->row & ECS_ROW_FLAGS_MASK;
            record_to_move->row = ECS_ROW_TO_RECORD(index, row_flags);
            ecs_assert(record_to_move->table != NULL, ECS_INTERNAL_ERROR, NULL);
            ecs_assert(record_to_move->table == table, ECS_INTERNAL_ERROR, NULL);
        }
    }     

    /* If the table is monitored indicate that there has been a change */
    flecs_table_mark_table_dirty(world, table, 0);    

    /* If table is empty, deactivate it */
    if (!count) {
        flecs_table_set_empty(world, table);
    }

    /* Destruct component data */
    ecs_type_info_t **type_info = table->type_info;
    ecs_vec_t *columns = data->columns;
    int32_t column_count = table->storage_count;
    int32_t i;

    /* If this is a table without lifecycle callbacks or special columns, take
     * fast path that just remove an element from the array(s) */
    if (!(table->flags & EcsTableIsComplex)) {
        if (index == count) {
            flecs_table_fast_delete_last(columns, column_count);
        } else {
            flecs_table_fast_delete(type_info, columns, column_count, index);
        }

        flecs_table_check_sanity(table);
        return;
    }

    ecs_id_t *ids = table->storage_ids;

    /* Last element, destruct & remove */
    if (index == count) {
        /* If table has component destructors, invoke */
        if (destruct && (table->flags & EcsTableHasDtors)) {            
            for (i = 0; i < column_count; i ++) {
                flecs_table_invoke_remove_hooks(world, table, type_info[i], &columns[i], 
                    &entity_to_delete, ids[i], index, 1, true);
            }
        }

        flecs_table_fast_delete_last(columns, column_count);

    /* Not last element, move last element to deleted element & destruct */
    } else {
        /* If table has component destructors, invoke */
        if ((table->flags & (EcsTableHasDtors | EcsTableHasMove))) {
            for (i = 0; i < column_count; i ++) {
                ecs_vec_t *column = &columns[i];
                ecs_type_info_t *ti = type_info[i];
                ecs_size_t size = ti->size;
                void *dst = ecs_vec_get(column, size, index);
                void *src = ecs_vec_last(column, size);
                
                ecs_iter_action_t on_remove = ti->hooks.on_remove;
                if (destruct && on_remove) {
                    flecs_table_invoke_hook(world, table, on_remove, EcsOnRemove,
                        column, &entity_to_delete, ids[i], index, 1, ti);
                }

                ecs_move_t move_dtor = ti->hooks.move_dtor;
                if (move_dtor) {
                    move_dtor(dst, src, 1, ti);
                } else {
                    ecs_os_memcpy(dst, src, size);
                }

                ecs_vec_remove_last(column);
            }
        } else {
            flecs_table_fast_delete(type_info, columns, column_count, index);
        }
    }

    /* Remove elements from switch columns */
    ecs_table__t *meta = table->_;
    ecs_switch_t *sw_columns = meta->sw_columns;
    int32_t sw_count = meta->sw_count;
    for (i = 0; i < sw_count; i ++) {
        flecs_switch_remove(&sw_columns[i], index);
    }

    /* Remove elements from bitset columns */
    ecs_bitset_t *bs_columns = meta->bs_columns;
    int32_t bs_count = meta->bs_count;
    for (i = 0; i < bs_count; i ++) {
        flecs_bitset_remove(&bs_columns[i], index);
    }

    flecs_table_check_sanity(table);
}

/* Move operation for tables that don't have any complex logic */
static
void flecs_table_fast_move(
    ecs_table_t *dst_table,
    int32_t dst_index,
    ecs_table_t *src_table,
    int32_t src_index)
{
    int32_t i_new = 0, dst_column_count = dst_table->storage_count;
    int32_t i_old = 0, src_column_count = src_table->storage_count;
    ecs_id_t *dst_ids = dst_table->storage_ids;
    ecs_id_t *src_ids = src_table->storage_ids;

    ecs_vec_t *src_columns = src_table->data.columns;
    ecs_vec_t *dst_columns = dst_table->data.columns;

    ecs_type_info_t **dst_type_info = dst_table->type_info;

    for (; (i_new < dst_column_count) && (i_old < src_column_count);) {
        ecs_id_t dst_id = dst_ids[i_new];
        ecs_id_t src_id = src_ids[i_old];

        if (dst_id == src_id) {
            ecs_vec_t *dst_column = &dst_columns[i_new];
            ecs_vec_t *src_column = &src_columns[i_old];
            ecs_type_info_t *ti = dst_type_info[i_new];
            int32_t size = ti->size;
            void *dst = ecs_vec_get(dst_column, size, dst_index);
            void *src = ecs_vec_get(src_column, size, src_index);
            ecs_os_memcpy(dst, src, size);
        }

        i_new += dst_id <= src_id;
        i_old += dst_id >= src_id;
    }
}

/* Move entity from src to dst table */
void flecs_table_move(
    ecs_world_t *world,
    ecs_entity_t dst_entity,
    ecs_entity_t src_entity,
    ecs_table_t *dst_table,
    int32_t dst_index,
    ecs_table_t *src_table,
    int32_t src_index,
    bool construct)
{
    ecs_assert(dst_table != NULL, ECS_INTERNAL_ERROR, NULL);
    ecs_assert(src_table != NULL, ECS_INTERNAL_ERROR, NULL);
    ecs_assert(!dst_table->_->lock, ECS_LOCKED_STORAGE, NULL);
    ecs_assert(!src_table->_->lock, ECS_LOCKED_STORAGE, NULL);

    ecs_assert(src_index >= 0, ECS_INTERNAL_ERROR, NULL);
    ecs_assert(dst_index >= 0, ECS_INTERNAL_ERROR, NULL);

    flecs_table_check_sanity(dst_table);
    flecs_table_check_sanity(src_table);

    if (!((dst_table->flags | src_table->flags) & EcsTableIsComplex)) {
        flecs_table_fast_move(dst_table, dst_index, src_table, src_index);
        flecs_table_check_sanity(dst_table);
        flecs_table_check_sanity(src_table);
        return;
    }

    flecs_table_move_switch_columns(dst_table, dst_index, src_table, src_index, 1, false);
    flecs_table_move_bitset_columns(dst_table, dst_index, src_table, src_index, 1, false);

    /* If the source and destination entities are the same, move component 
     * between tables. If the entities are not the same (like when cloning) use
     * a copy. */
    bool same_entity = dst_entity == src_entity;

    /* Call move_dtor for moved away from storage only if the entity is at the
     * last index in the source table. If it isn't the last entity, the last 
     * entity in the table will be moved to the src storage, which will take
     * care of cleaning up resources. */
    bool use_move_dtor = ecs_table_count(src_table) == (src_index + 1);

    ecs_type_info_t **dst_type_info = dst_table->type_info;
    ecs_type_info_t **src_type_info = src_table->type_info;

    int32_t i_new = 0, dst_column_count = dst_table->storage_count;
    int32_t i_old = 0, src_column_count = src_table->storage_count;
    ecs_id_t *dst_ids = dst_table->storage_ids;
    ecs_id_t *src_ids = src_table->storage_ids;

    ecs_vec_t *src_columns = src_table->data.columns;
    ecs_vec_t *dst_columns = dst_table->data.columns;

    for (; (i_new < dst_column_count) && (i_old < src_column_count); ) {
        ecs_id_t dst_id = dst_ids[i_new];
        ecs_id_t src_id = src_ids[i_old];

        if (dst_id == src_id) {
            ecs_vec_t *dst_column = &dst_columns[i_new];
            ecs_vec_t *src_column = &src_columns[i_old];
            ecs_type_info_t *ti = dst_type_info[i_new];
            int32_t size = ti->size;

            ecs_assert(size != 0, ECS_INTERNAL_ERROR, NULL);
            void *dst = ecs_vec_get(dst_column, size, dst_index);
            void *src = ecs_vec_get(src_column, size, src_index);

            if (same_entity) {
                ecs_move_t move = ti->hooks.move_ctor;
                if (use_move_dtor || !move) {
                    /* Also use move_dtor if component doesn't have a move_ctor
                     * registered, to ensure that the dtor gets called to 
                     * cleanup resources. */
                    move = ti->hooks.ctor_move_dtor;
                }

                if (move) {
                    move(dst, src, 1, ti);
                } else {
                    ecs_os_memcpy(dst, src, size);
                }
            } else {
                ecs_copy_t copy = ti->hooks.copy_ctor;
                if (copy) {
                    copy(dst, src, 1, ti);
                } else {
                    ecs_os_memcpy(dst, src, size);
                }
            }
        } else {
            if (dst_id < src_id) {
                flecs_table_invoke_add_hooks(world, dst_table, dst_type_info[i_new],
                    &dst_columns[i_new], &dst_entity, dst_id, 
                        dst_index, 1, construct);
            } else {
                flecs_table_invoke_remove_hooks(world, src_table, src_type_info[i_old],
                    &src_columns[i_old], &src_entity, src_id, 
                        src_index, 1, use_move_dtor);
            }
        }

        i_new += dst_id <= src_id;
        i_old += dst_id >= src_id;
    }

    for (; (i_new < dst_column_count); i_new ++) {
        flecs_table_invoke_add_hooks(world, dst_table, dst_type_info[i_new],
            &dst_columns[i_new], &dst_entity, dst_ids[i_new], dst_index, 1,
                construct);
    }

    for (; (i_old < src_column_count); i_old ++) {
        flecs_table_invoke_remove_hooks(world, src_table, src_type_info[i_old],
            &src_columns[i_old], &src_entity, src_ids[i_old], 
                src_index, 1, use_move_dtor);
    }

    flecs_table_check_sanity(dst_table);
    flecs_table_check_sanity(src_table);
}

/* Append n entities to table */
int32_t flecs_table_appendn(
    ecs_world_t *world,
    ecs_table_t *table,
    ecs_data_t *data,
    int32_t to_add,
    const ecs_entity_t *ids)
{
    ecs_assert(!table->_->lock, ECS_LOCKED_STORAGE, NULL);

    flecs_table_check_sanity(table);
    int32_t cur_count = flecs_table_data_count(data);
    int32_t result = flecs_table_grow_data(
        world, table, data, to_add, cur_count + to_add, ids);
    flecs_table_check_sanity(table);

    return result;
}

/* Set allocated table size */
void flecs_table_set_size(
    ecs_world_t *world,
    ecs_table_t *table,
    ecs_data_t *data,
    int32_t size)
{
    ecs_assert(table != NULL, ECS_LOCKED_STORAGE, NULL);
    ecs_assert(!table->_->lock, ECS_LOCKED_STORAGE, NULL);

    flecs_table_check_sanity(table);

    int32_t cur_count = flecs_table_data_count(data);

    if (cur_count < size) {
        flecs_table_grow_data(world, table, data, 0, size, NULL);
        flecs_table_check_sanity(table);
    }
}

/* Shrink table storage to fit number of entities */
bool flecs_table_shrink(
    ecs_world_t *world,
    ecs_table_t *table)
{
    ecs_assert(table != NULL, ECS_LOCKED_STORAGE, NULL);
    ecs_assert(!table->_->lock, ECS_LOCKED_STORAGE, NULL);
    (void)world;

    flecs_table_check_sanity(table);

    ecs_data_t *data = &table->data;
    bool has_payload = data->entities.array != NULL;
    ecs_vec_reclaim_t(&world->allocator, &data->entities, ecs_entity_t);
    ecs_vec_reclaim_t(&world->allocator, &data->records, ecs_record_t*);

    int32_t i, count = table->storage_count;
    ecs_type_info_t **type_info = table->type_info;
    for (i = 0; i < count; i ++) {
        ecs_vec_t *column = &data->columns[i];
        ecs_type_info_t *ti = type_info[i];
        ecs_vec_reclaim(&world->allocator, column, ti->size);
    }

    return has_payload;
}

/* Return number of entities in table */
int32_t flecs_table_data_count(
    const ecs_data_t *data)
{
    return data ? data->entities.count : 0;
}

/* Swap operation for switch (union relationship) columns */
static
void flecs_table_swap_switch_columns(
    ecs_table_t *table,
    int32_t row_1,
    int32_t row_2)
{
    int32_t i = 0, column_count = table->_->sw_count;
    if (!column_count) {
        return;
    }

    ecs_switch_t *columns = table->_->sw_columns;

    for (i = 0; i < column_count; i ++) {
        ecs_switch_t *sw = &columns[i];
        flecs_switch_swap(sw, row_1, row_2);
    }
}

/* Swap operation for bitset (toggle component) columns */
static
void flecs_table_swap_bitset_columns(
    ecs_table_t *table,
    int32_t row_1,
    int32_t row_2)
{
    int32_t i = 0, column_count = table->_->bs_count;
    if (!column_count) {
        return;
    }

    ecs_bitset_t *columns = table->_->bs_columns;

    for (i = 0; i < column_count; i ++) {
        ecs_bitset_t *bs = &columns[i];
        flecs_bitset_swap(bs, row_1, row_2);
    }
}

/* Swap two rows in a table. Used for table sorting. */
void flecs_table_swap(
    ecs_world_t *world,
    ecs_table_t *table,
    int32_t row_1,
    int32_t row_2)
{    
    (void)world;

    ecs_assert(!table->_->lock, ECS_LOCKED_STORAGE, NULL);
    ecs_assert(row_1 >= 0, ECS_INTERNAL_ERROR, NULL);
    ecs_assert(row_2 >= 0, ECS_INTERNAL_ERROR, NULL);

    flecs_table_check_sanity(table);
    
    if (row_1 == row_2) {
        return;
    }

    /* If the table is monitored indicate that there has been a change */
    flecs_table_mark_table_dirty(world, table, 0);    

    ecs_entity_t *entities = table->data.entities.array;
    ecs_entity_t e1 = entities[row_1];
    ecs_entity_t e2 = entities[row_2];

    ecs_record_t **records = table->data.records.array;
    ecs_record_t *record_ptr_1 = records[row_1];
    ecs_record_t *record_ptr_2 = records[row_2];

    ecs_assert(record_ptr_1 != NULL, ECS_INTERNAL_ERROR, NULL);
    ecs_assert(record_ptr_2 != NULL, ECS_INTERNAL_ERROR, NULL);

    /* Keep track of whether entity is watched */
    uint32_t flags_1 = ECS_RECORD_TO_ROW_FLAGS(record_ptr_1->row);
    uint32_t flags_2 = ECS_RECORD_TO_ROW_FLAGS(record_ptr_2->row);

    /* Swap entities & records */
    entities[row_1] = e2;
    entities[row_2] = e1;
    record_ptr_1->row = ECS_ROW_TO_RECORD(row_2, flags_1);
    record_ptr_2->row = ECS_ROW_TO_RECORD(row_1, flags_2);
    records[row_1] = record_ptr_2;
    records[row_2] = record_ptr_1;

    flecs_table_swap_switch_columns(table, row_1, row_2);
    flecs_table_swap_bitset_columns(table, row_1, row_2);

    ecs_vec_t *columns = table->data.columns;
    if (!columns) {
        flecs_table_check_sanity(table);
        return;
    }

    ecs_type_info_t **type_info = table->type_info;

    /* Find the maximum size of column elements
     * and allocate a temporary buffer for swapping */
    int32_t i, temp_buffer_size = ECS_SIZEOF(uint64_t), column_count = table->storage_count;
    for (i = 0; i < column_count; i++) {
        ecs_type_info_t* ti = type_info[i];
        temp_buffer_size = ECS_MAX(temp_buffer_size, ti->size);
    }

    void* tmp = ecs_os_alloca(temp_buffer_size);

    /* Swap columns */
    for (i = 0; i < column_count; i ++) {
        ecs_type_info_t *ti = type_info[i];
        int32_t size = ti->size;
        ecs_assert(size != 0, ECS_INTERNAL_ERROR, NULL);

        void *ptr = columns[i].array;

        void *el_1 = ECS_ELEM(ptr, size, row_1);
        void *el_2 = ECS_ELEM(ptr, size, row_2);

        ecs_os_memcpy(tmp, el_1, size);
        ecs_os_memcpy(el_1, el_2, size);
        ecs_os_memcpy(el_2, tmp, size);
    }

    flecs_table_check_sanity(table);
}

/* Merge data from one table column into other table column */
static
void flecs_table_merge_column(
    ecs_world_t *world,
    ecs_vec_t *dst,
    ecs_vec_t *src,
    int32_t size,
    int32_t column_size,
    ecs_type_info_t *ti)
{
    int32_t dst_count = dst->count;

    if (!dst_count) {
        ecs_vec_fini(&world->allocator, dst, size);
        *dst = *src;
        src->array = NULL;
        src->count = 0;
        src->size = 0;
    
    /* If the new table is not empty, copy the contents from the
     * src into the dst. */
    } else {
        int32_t src_count = src->count;

        if (ti) {
            flecs_table_grow_column(world, dst, ti, src_count, 
                column_size, true);
        } else {
            if (column_size) {
                ecs_vec_set_size(&world->allocator, 
                    dst, size, column_size);
            }
            ecs_vec_set_count(&world->allocator, 
                dst, size, dst_count + src_count);
        }

        void *dst_ptr = ECS_ELEM(dst->array, size, dst_count);
        void *src_ptr = src->array;

        /* Move values into column */
        ecs_move_t move = NULL;
        if (ti) {
            move = ti->hooks.move_dtor;
        }
        if (move) {
            move(dst_ptr, src_ptr, src_count, ti);
        } else {
            ecs_os_memcpy(dst_ptr, src_ptr, size * src_count);
        }

        ecs_vec_fini(&world->allocator, src, size);
    }
}

/* Merge storage of two tables. */
static
void flecs_table_merge_data(
    ecs_world_t *world,
    ecs_table_t *dst_table,
    ecs_table_t *src_table,
    int32_t src_count,
    int32_t dst_count,
    ecs_data_t *src_data,
    ecs_data_t *dst_data)
{
    int32_t i_new = 0, dst_column_count = dst_table->storage_count;
    int32_t i_old = 0, src_column_count = src_table->storage_count;
    ecs_id_t *dst_ids = dst_table->storage_ids;
    ecs_id_t *src_ids = src_table->storage_ids;

    ecs_type_info_t **dst_type_info = dst_table->type_info;
    ecs_type_info_t **src_type_info = src_table->type_info;

    ecs_vec_t *src = src_data->columns;
    ecs_vec_t *dst = dst_data->columns;

    ecs_assert(!dst_column_count || dst, ECS_INTERNAL_ERROR, NULL);

    if (!src_count) {
        return;
    }

    /* Merge entities */
    flecs_table_merge_column(world, &dst_data->entities, &src_data->entities, 
        ECS_SIZEOF(ecs_entity_t), 0, NULL);
    ecs_assert(dst_data->entities.count == src_count + dst_count, 
        ECS_INTERNAL_ERROR, NULL);
    int32_t column_size = dst_data->entities.size;
    ecs_allocator_t *a = &world->allocator;

    /* Merge record pointers */
    flecs_table_merge_column(world, &dst_data->records, &src_data->records, 
        ECS_SIZEOF(ecs_record_t*), 0, NULL);

    for (; (i_new < dst_column_count) && (i_old < src_column_count); ) {
        ecs_id_t dst_id = dst_ids[i_new];
        ecs_id_t src_id = src_ids[i_old];
        ecs_type_info_t *dst_ti = dst_type_info[i_new];
        int32_t size = dst_ti->size;
        ecs_assert(size != 0, ECS_INTERNAL_ERROR, NULL);

        if (dst_id == src_id) {
            flecs_table_merge_column(world, &dst[i_new], &src[i_old], size, column_size, dst_ti);
            flecs_table_mark_table_dirty(world, dst_table, i_new + 1);
            ecs_assert(dst[i_new].size == dst_data->entities.size, 
                ECS_INTERNAL_ERROR, NULL);

            i_new ++;
            i_old ++;
        } else if (dst_id < src_id) {
            /* New column, make sure vector is large enough. */
            ecs_vec_t *column = &dst[i_new];
            ecs_vec_set_size(a, column, size, column_size);
            ecs_vec_set_count(a, column, size, src_count + dst_count);
            flecs_table_invoke_ctor(dst_ti, column, dst_count, src_count);
            i_new ++;
        } else if (dst_id > src_id) {
            /* Old column does not occur in new table, destruct */
            ecs_vec_t *column = &src[i_old];
            ecs_type_info_t *ti = src_type_info[i_old];
            flecs_table_invoke_dtor(ti, column, 0, src_count);
            ecs_vec_fini(a, column, ti->size);
            i_old ++;
        }
    }

    flecs_table_move_switch_columns(dst_table, dst_count, src_table, 0, src_count, true);
    flecs_table_move_bitset_columns(dst_table, dst_count, src_table, 0, src_count, true);

    /* Initialize remaining columns */
    for (; i_new < dst_column_count; i_new ++) {
        ecs_vec_t *column = &dst[i_new];
        ecs_type_info_t *ti = dst_type_info[i_new];
        int32_t size = ti->size;
        ecs_assert(size != 0, ECS_INTERNAL_ERROR, NULL);
        ecs_vec_set_size(a, column, size, column_size);
        ecs_vec_set_count(a, column, size, src_count + dst_count);
        flecs_table_invoke_ctor(ti, column, dst_count, src_count);
    }

    /* Destruct remaining columns */
    for (; i_old < src_column_count; i_old ++) {
        ecs_vec_t *column = &src[i_old];
        ecs_type_info_t *ti = src_type_info[i_old];
        flecs_table_invoke_dtor(ti, column, 0, src_count);
        ecs_vec_fini(a, column, ti->size);
    }    

    /* Mark entity column as dirty */
    flecs_table_mark_table_dirty(world, dst_table, 0); 
}

/* Merge source table into destination table. This typically happens as result
 * of a bulk operation, like when a component is removed from all entities in 
 * the source table (like for the Remove OnDelete policy). */
void flecs_table_merge(
    ecs_world_t *world,
    ecs_table_t *dst_table,
    ecs_table_t *src_table,
    ecs_data_t *dst_data,
    ecs_data_t *src_data)
{
    ecs_assert(src_table != NULL, ECS_INTERNAL_ERROR, NULL);
    ecs_assert(!src_table->_->lock, ECS_LOCKED_STORAGE, NULL);

    flecs_table_check_sanity(src_table);
    flecs_table_check_sanity(dst_table);
    
    bool move_data = false;
    
    /* If there is nothing to merge to, just clear the old table */
    if (!dst_table) {
        flecs_table_clear_data(world, src_table, src_data);
        flecs_table_check_sanity(src_table);
        return;
    } else {
        ecs_assert(!dst_table->_->lock, ECS_LOCKED_STORAGE, NULL);
    }

    /* If there is no data to merge, drop out */
    if (!src_data) {
        return;
    }

    if (!dst_data) {
        dst_data = &dst_table->data;
        if (dst_table == src_table) {
            move_data = true;
        }
    }

    ecs_entity_t *src_entities = src_data->entities.array;
    int32_t src_count = src_data->entities.count;
    int32_t dst_count = dst_data->entities.count;
    ecs_record_t **src_records = src_data->records.array;

    /* First, update entity index so old entities point to new type */
    int32_t i;
    for(i = 0; i < src_count; i ++) {
        ecs_record_t *record;
        if (dst_table != src_table) {
            record = src_records[i];
            ecs_assert(record != NULL, ECS_INTERNAL_ERROR, NULL);
        } else {
            record = flecs_entities_ensure(world, src_entities[i]);
        }

        uint32_t flags = ECS_RECORD_TO_ROW_FLAGS(record->row);
        record->row = ECS_ROW_TO_RECORD(dst_count + i, flags);
        record->table = dst_table;
    }

    /* Merge table columns */
    if (move_data) {
        *dst_data = *src_data;
    } else {
        flecs_table_merge_data(world, dst_table, src_table, src_count, dst_count, 
            src_data, dst_data);
    }

    if (src_count) {
        if (!dst_count) {
            flecs_table_set_empty(world, dst_table);
        }
        flecs_table_set_empty(world, src_table);

        flecs_table_traversable_add(dst_table, src_table->_->traversable_count);
        flecs_table_traversable_add(src_table, -src_table->_->traversable_count);
        ecs_assert(src_table->_->traversable_count == 0, ECS_INTERNAL_ERROR, NULL);
    }

    flecs_table_check_sanity(src_table);
    flecs_table_check_sanity(dst_table);
}

/* Replace data with other data. Used by snapshots to restore previous state. */
void flecs_table_replace_data(
    ecs_world_t *world,
    ecs_table_t *table,
    ecs_data_t *data)
{
    int32_t prev_count = 0;
    ecs_data_t *table_data = &table->data;
    ecs_assert(!data || data != table_data, ECS_INTERNAL_ERROR, NULL);
    ecs_assert(!table->_->lock, ECS_LOCKED_STORAGE, NULL);

    flecs_table_check_sanity(table);

    prev_count = table_data->entities.count;
    flecs_table_notify_on_remove(world, table, table_data);
    flecs_table_clear_data(world, table, table_data);

    if (data) {
        table->data = *data;
    } else {
        flecs_table_init_data(world, table);
    }

    int32_t count = ecs_table_count(table);

    if (!prev_count && count) {
        flecs_table_set_empty(world, table);
    } else if (prev_count && !count) {
        flecs_table_set_empty(world, table);
    }

    flecs_table_check_sanity(table);
}

/* Internal mechanism for propagating information to tables */
void flecs_table_notify(
    ecs_world_t *world,
    ecs_table_t *table,
    ecs_table_event_t *event)
{
    if (world->flags & EcsWorldFini) {
        return;
    }

    switch(event->kind) {
    case EcsTableTriggersForId:
        flecs_table_add_trigger_flags(world, table, event->event);
        break;
    case EcsTableNoTriggersForId:
        break;
    }
}

/* -- Public API -- */

void ecs_table_lock(
    ecs_world_t *world,
    ecs_table_t *table)
{
    if (table) {
        if (ecs_poly_is(world, ecs_world_t) && !(world->flags & EcsWorldReadonly)) {
            table->_->lock ++;
        }
    }
}

void ecs_table_unlock(
    ecs_world_t *world,
    ecs_table_t *table)
{
    if (table) {
        if (ecs_poly_is(world, ecs_world_t) && !(world->flags & EcsWorldReadonly)) {
            table->_->lock --;
            ecs_assert(table->_->lock >= 0, ECS_INVALID_OPERATION, NULL);
        }
    }
}

bool ecs_table_has_module(
    ecs_table_t *table)
{
    return table->flags & EcsTableHasModule;
}

ecs_vec_t* ecs_table_column_for_id(
    const ecs_world_t *world,
    const ecs_table_t *table,
    ecs_id_t id)
{
    ecs_table_t *storage_table = table->storage_table;
    if (!storage_table) {
        return NULL;
    }

    ecs_table_record_t *tr = flecs_table_record_get(world, storage_table, id);
    if (tr) {
        return &table->data.columns[tr->column];
    }

    return NULL;
}

int32_t ecs_table_count(
    const ecs_table_t *table)
{
    ecs_assert(table != NULL, ECS_INTERNAL_ERROR, NULL);
    return flecs_table_data_count(&table->data);
}

const ecs_type_t* ecs_table_get_type(
    const ecs_table_t *table)
{
    if (table) {
        return &table->type;
    } else {
        return NULL;
    }
}

ecs_table_t* ecs_table_get_storage_table(
    const ecs_table_t *table)
{
    return table->storage_table;
}

int32_t ecs_table_type_to_storage_index(
    const ecs_table_t *table,
    int32_t index)
{
    ecs_assert(index >= 0, ECS_INVALID_PARAMETER, NULL);
    ecs_check(index < table->type.count, ECS_INVALID_PARAMETER, NULL);
    int32_t *storage_map = table->storage_map;
    if (storage_map) {
        return storage_map[index];
    }
error:
    return -1;
}

int32_t ecs_table_storage_to_type_index(
    const ecs_table_t *table,
    int32_t index)
{
    ecs_check(index < table->storage_count, ECS_INVALID_PARAMETER, NULL);
    ecs_check(table->storage_map != NULL, ECS_INVALID_PARAMETER, NULL);
    int32_t offset = table->type.count;
    return table->storage_map[offset + index];
error:
    return -1;
}

int32_t flecs_table_column_to_union_index(
    const ecs_table_t *table,
    int32_t column)
{
    int32_t sw_count = table->_->sw_count;
    if (sw_count) {
        int32_t sw_offset = table->_->sw_offset;
        if (column >= sw_offset && column < (sw_offset + sw_count)){
            return column - sw_offset;
        }
    }
    return -1;
}

void* ecs_table_get_column(
    const ecs_table_t *table,
    int32_t index,
    int32_t offset)
{
    ecs_check(table != NULL, ECS_INVALID_PARAMETER, NULL);
    ecs_check(index < table->type.count, ECS_INVALID_PARAMETER, NULL);
    ecs_check(table->storage_map != NULL, ECS_INVALID_PARAMETER, NULL);

    int32_t storage_index = table->storage_map[index];
    if (storage_index == -1) {
        return NULL;
    }

    void *result = table->data.columns[storage_index].array;
    if (offset) {
        ecs_size_t size = table->type_info[storage_index]->size;
        result = ECS_ELEM(result, size, offset);
    }

    return result;
error:
    return NULL;
}

size_t ecs_table_get_column_size(
    const ecs_table_t *table,
    int32_t index)
{
    ecs_check(table != NULL, ECS_INVALID_PARAMETER, NULL);
    ecs_check(index < table->type.count, ECS_INVALID_PARAMETER, NULL);
    ecs_check(table->storage_map != NULL, ECS_INVALID_PARAMETER, NULL);

    int32_t storage_index = table->storage_map[index];
    if (storage_index == -1) {
        return 0;
    }

    return flecs_ito(size_t, table->type_info[storage_index]->size);
error:
    return 0;
}

int32_t ecs_table_get_index(
    const ecs_world_t *world,
    const ecs_table_t *table,
    ecs_id_t id)
{
    ecs_poly_assert(world, ecs_world_t);
    ecs_check(table != NULL, ECS_INVALID_PARAMETER, NULL);
    ecs_check(ecs_id_is_valid(world, id), ECS_INVALID_PARAMETER, NULL);

    ecs_id_record_t *idr = flecs_id_record_get(world, id);
    if (!idr) {
        return -1;
    }

    const ecs_table_record_t *tr = flecs_id_record_get_table(idr, table);
    if (!tr) {
        return -1;
    }

    return tr->column;
error:
    return -1;
}

bool ecs_table_has_id(
    const ecs_world_t *world,
    const ecs_table_t *table,
    ecs_id_t id)
{
    return ecs_table_get_index(world, table, id) != -1;
}

void* ecs_table_get_id(
    const ecs_world_t *world,
    const ecs_table_t *table,
    ecs_id_t id,
    int32_t offset)
{
    ecs_check(world != NULL, ECS_INVALID_PARAMETER, NULL);
    ecs_check(table != NULL, ECS_INVALID_PARAMETER, NULL);
    ecs_check(ecs_id_is_valid(world, id), ECS_INVALID_PARAMETER, NULL);

    world = ecs_get_world(world);

    int32_t index = ecs_table_get_index(world, table, id);
    if (index == -1) {
        return NULL;
    }

    return ecs_table_get_column(table, index, offset);
error:
    return NULL;
}

int32_t ecs_table_get_depth(
    const ecs_world_t *world,
    const ecs_table_t *table,
    ecs_entity_t rel)
{
    ecs_check(world != NULL, ECS_INVALID_PARAMETER, NULL);
    ecs_check(table != NULL, ECS_INVALID_PARAMETER, NULL);
    ecs_check(ecs_id_is_valid(world, rel), ECS_INVALID_PARAMETER, NULL);
    ecs_check(ecs_has_id(world, rel, EcsAcyclic), ECS_INVALID_PARAMETER, NULL);

    world = ecs_get_world(world);

    return flecs_relation_depth(world, rel, table);
error:
    return -1;
}

void ecs_table_swap_rows(
    ecs_world_t* world,
    ecs_table_t* table,
    int32_t row_1,
    int32_t row_2)
{
    flecs_table_swap(world, table, row_1, row_2);
}

int32_t flecs_table_observed_count(
    const ecs_table_t *table)
{
    return table->_->traversable_count;
}

void* ecs_record_get_column(
    const ecs_record_t *r,
    int32_t column,
    size_t c_size)
{
    (void)c_size;
    ecs_table_t *table = r->table;

    ecs_check(column < table->storage_count, ECS_INVALID_PARAMETER, NULL);
    ecs_type_info_t *ti = table->type_info[column];
    ecs_vec_t *c = &table->data.columns[column];
    ecs_assert(c != NULL, ECS_INTERNAL_ERROR, NULL);

    ecs_check(!flecs_utosize(c_size) || flecs_utosize(c_size) == ti->size, 
        ECS_INVALID_PARAMETER, NULL);

    return ecs_vec_get(c, ti->size, ECS_RECORD_TO_ROW(r->row));
error:
    return NULL;
}

ecs_record_t* ecs_record_find(
    const ecs_world_t *world,
    ecs_entity_t entity)
{
    ecs_check(world != NULL, ECS_INVALID_PARAMETER, NULL);
    ecs_check(entity != 0, ECS_INVALID_PARAMETER, NULL);

    world = ecs_get_world(world);

    ecs_record_t *r = flecs_entities_get(world, entity);
    if (r) {
        return r;
    }
error:
    return NULL;
}

/**
 * @file poly.c
 * @brief Functions for managing poly objects.
 * 
 * The poly framework makes it possible to generalize common functionality for
 * different kinds of API objects, as well as improved type safety checks. Poly
 * objects have a header that identifiers what kind of object it is. This can
 * then be used to discover a set of "mixins" implemented by the type.
 * 
 * Mixins are like a vtable, but for members. Each type populates the table with
 * offsets to the members that correspond with the mixin. If an entry in the
 * mixin table is not set, the type does not support the mixin.
 * 
 * An example is the Iterable mixin, which makes it possible to create an 
 * iterator for any poly object (like filters, queries, the world) that 
 * implements the Iterable mixin.
 */


static const char* mixin_kind_str[] = {
    [EcsMixinWorld] = "world",
    [EcsMixinEntity] = "entity",
    [EcsMixinObservable] = "observable",
    [EcsMixinIterable] = "iterable",
    [EcsMixinDtor] = "dtor",
    [EcsMixinMax] = "max (should never be requested by application)"
};

ecs_mixins_t ecs_world_t_mixins = {
    .type_name = "ecs_world_t",
    .elems = {
        [EcsMixinWorld] = offsetof(ecs_world_t, self),
        [EcsMixinObservable] = offsetof(ecs_world_t, observable),
        [EcsMixinIterable] = offsetof(ecs_world_t, iterable)
    }
};

ecs_mixins_t ecs_stage_t_mixins = {
    .type_name = "ecs_stage_t",
    .elems = {
        [EcsMixinWorld] = offsetof(ecs_stage_t, world)
    }
};

ecs_mixins_t ecs_query_t_mixins = {
    .type_name = "ecs_query_t",
    .elems = {
        [EcsMixinWorld] = offsetof(ecs_query_t, filter.world),
        [EcsMixinEntity] = offsetof(ecs_query_t, filter.entity),
        [EcsMixinIterable] = offsetof(ecs_query_t, iterable),
        [EcsMixinDtor] = offsetof(ecs_query_t, dtor)
    }
};

ecs_mixins_t ecs_observer_t_mixins = {
    .type_name = "ecs_observer_t",
    .elems = {
        [EcsMixinWorld] = offsetof(ecs_observer_t, filter.world),
        [EcsMixinEntity] = offsetof(ecs_observer_t, filter.entity),
        [EcsMixinDtor] = offsetof(ecs_observer_t, dtor)
    }
};

ecs_mixins_t ecs_filter_t_mixins = {
    .type_name = "ecs_filter_t",
    .elems = {
        [EcsMixinWorld] = offsetof(ecs_filter_t, world),
        [EcsMixinEntity] = offsetof(ecs_filter_t, entity),
        [EcsMixinIterable] = offsetof(ecs_filter_t, iterable),
        [EcsMixinDtor] = offsetof(ecs_filter_t, dtor)
    }
};

static
void* assert_mixin(
    const ecs_poly_t *poly,
    ecs_mixin_kind_t kind)
{
    ecs_assert(poly != NULL, ECS_INVALID_PARAMETER, NULL);
    ecs_assert(kind < EcsMixinMax, ECS_INVALID_PARAMETER, NULL);
    
    const ecs_header_t *hdr = poly;
    ecs_assert(hdr != NULL, ECS_INVALID_PARAMETER, NULL);
    ecs_assert(hdr->magic == ECS_OBJECT_MAGIC, ECS_INVALID_PARAMETER, NULL);

    const ecs_mixins_t *mixins = hdr->mixins;
    ecs_assert(mixins != NULL, ECS_INVALID_PARAMETER, NULL);

    ecs_size_t offset = mixins->elems[kind];
    ecs_assert(offset != 0, ECS_INVALID_PARAMETER, 
        "mixin %s not available for type %s",
            mixin_kind_str[kind], mixins ? mixins->type_name : "unknown");
    (void)mixin_kind_str;

    /* Object has mixin, return its address */
    return ECS_OFFSET(hdr, offset);
}

void* _ecs_poly_init(
    ecs_poly_t *poly,
    int32_t type,
    ecs_size_t size,
    ecs_mixins_t *mixins)
{
    ecs_assert(poly != NULL, ECS_INVALID_PARAMETER, NULL);

    ecs_header_t *hdr = poly;
    ecs_os_memset(poly, 0, size);

    hdr->magic = ECS_OBJECT_MAGIC;
    hdr->type = type;
    hdr->mixins = mixins;

    return poly;
}

void _ecs_poly_fini(
    ecs_poly_t *poly,
    int32_t type)
{
    ecs_assert(poly != NULL, ECS_INVALID_PARAMETER, NULL);
    (void)type;

    ecs_header_t *hdr = poly;

    /* Don't deinit poly that wasn't initialized */
    ecs_assert(hdr->magic == ECS_OBJECT_MAGIC, ECS_INVALID_PARAMETER, NULL);
    ecs_assert(hdr->type == type, ECS_INVALID_PARAMETER, NULL);
    hdr->magic = 0;
}

EcsPoly* _ecs_poly_bind(
    ecs_world_t *world,
    ecs_entity_t entity,
    ecs_entity_t tag)
{
    /* Add tag to the entity for easy querying. This will make it possible to
     * query for `Query` instead of `(Poly, Query) */
    if (!ecs_has_id(world, entity, tag)) {
        ecs_add_id(world, entity, tag);
    }

    /* Never defer creation of a poly object */
    bool deferred = false;
    if (ecs_is_deferred(world)) {
        deferred = true;
        ecs_defer_suspend(world);
    }

    /* If this is a new poly, leave the actual creation up to the caller so they
     * call tell the difference between a create or an update */
    EcsPoly *result = ecs_get_mut_pair(world, entity, EcsPoly, tag);

    if (deferred) {
        ecs_defer_resume(world);
    }

    return result;
}

void _ecs_poly_modified(
    ecs_world_t *world,
    ecs_entity_t entity,
    ecs_entity_t tag)
{
    ecs_modified_pair(world, entity, ecs_id(EcsPoly), tag);
}

const EcsPoly* _ecs_poly_bind_get(
    const ecs_world_t *world,
    ecs_entity_t entity,
    ecs_entity_t tag)
{
    return ecs_get_pair(world, entity, EcsPoly, tag);
}

ecs_poly_t* _ecs_poly_get(
    const ecs_world_t *world,
    ecs_entity_t entity,
    ecs_entity_t tag)
{
    const EcsPoly *p = _ecs_poly_bind_get(world, entity, tag);
    if (p) {
        return p->poly;
    }
    return NULL;
}

#define assert_object(cond, file, line, type_name)\
    _ecs_assert((cond), ECS_INVALID_PARAMETER, #cond, file, line, type_name);\
    assert(cond)

#ifndef FLECS_NDEBUG
void* _ecs_poly_assert(
    const ecs_poly_t *poly,
    int32_t type,
    const char *file,
    int32_t line)
{
    assert_object(poly != NULL, file, line, 0);
    
    const ecs_header_t *hdr = poly;
    const char *type_name = hdr->mixins->type_name;
    assert_object(hdr->magic == ECS_OBJECT_MAGIC, file, line, type_name);
    assert_object(hdr->type == type, file, line, type_name);
    return (ecs_poly_t*)poly;
}
#endif

bool _ecs_poly_is(
    const ecs_poly_t *poly,
    int32_t type)
{
    ecs_assert(poly != NULL, ECS_INVALID_PARAMETER, NULL);

    const ecs_header_t *hdr = poly;
    ecs_assert(hdr->magic == ECS_OBJECT_MAGIC, ECS_INVALID_PARAMETER, NULL);
    return hdr->type == type;    
}

ecs_iterable_t* ecs_get_iterable(
    const ecs_poly_t *poly)
{
    return (ecs_iterable_t*)assert_mixin(poly, EcsMixinIterable);
}

ecs_observable_t* ecs_get_observable(
    const ecs_poly_t *poly)
{
    return (ecs_observable_t*)assert_mixin(poly, EcsMixinObservable);
}

const ecs_world_t* ecs_get_world(
    const ecs_poly_t *poly)
{
    if (((ecs_header_t*)poly)->type == ecs_world_t_magic) {
        return poly;
    }
    return *(ecs_world_t**)assert_mixin(poly, EcsMixinWorld);
}

ecs_entity_t ecs_get_entity(
    const ecs_poly_t *poly)
{
    return *(ecs_entity_t*)assert_mixin(poly, EcsMixinEntity);
}

ecs_poly_dtor_t* ecs_get_dtor(
    const ecs_poly_t *poly)
{
    return (ecs_poly_dtor_t*)assert_mixin(poly, EcsMixinDtor);
}

/**
 * @file entity.c
 * @brief Entity API.
 * 
 * This file contains the implementation for the entity API, which includes 
 * creating/deleting entities, adding/removing/setting components, instantiating
 * prefabs, and several other APIs for retrieving entity data.
 * 
 * The file also contains the implementation of the command buffer, which is 
 * located here so it can call functions private to the compilation unit.
 */

#include <ctype.h>

static
const ecs_entity_t* flecs_bulk_new(
    ecs_world_t *world,
    ecs_table_t *table,
    const ecs_entity_t *entities,
    ecs_type_t *component_ids,
    int32_t count,
    void **c_info,
    bool move,
    int32_t *row_out,
    ecs_table_diff_t *diff);

typedef struct {
    ecs_type_info_t *ti;
    void *ptr;
} flecs_component_ptr_t;

static
flecs_component_ptr_t flecs_get_component_w_index(
    ecs_table_t *table,
    int32_t column_index,
    int32_t row)
{
    ecs_check(column_index < table->storage_count, ECS_NOT_A_COMPONENT, NULL);
    ecs_type_info_t *ti = table->type_info[column_index];
    ecs_vec_t *column = &table->data.columns[column_index];
    return (flecs_component_ptr_t){
        .ti = ti,
        .ptr = ecs_vec_get(column, ti->size, row)
    };
error:
    return (flecs_component_ptr_t){0};
}

static
flecs_component_ptr_t flecs_get_component_ptr(
    const ecs_world_t *world,
    ecs_table_t *table,
    int32_t row,
    ecs_id_t id)
{
    ecs_assert(table != NULL, ECS_INTERNAL_ERROR, NULL);
    ecs_assert(id != 0, ECS_INVALID_PARAMETER, NULL);

    if (!table->storage_table) {
        ecs_check(ecs_search(world, table, id, 0) == -1, 
            ECS_NOT_A_COMPONENT, NULL);
        return (flecs_component_ptr_t){0};
    }

    ecs_table_record_t *tr = flecs_table_record_get(
        world, table->storage_table, id);
    if (!tr) {
        ecs_check(ecs_search(world, table, id, 0) == -1, 
            ECS_NOT_A_COMPONENT, NULL);
       return (flecs_component_ptr_t){0};
    }

    return flecs_get_component_w_index(table, tr->column, row);
error:
    return (flecs_component_ptr_t){0};
}

static
void* flecs_get_component(
    const ecs_world_t *world,
    ecs_table_t *table,
    int32_t row,
    ecs_id_t id)
{
    return flecs_get_component_ptr(world, table, row, id).ptr;
}

void* flecs_get_base_component(
    const ecs_world_t *world,
    ecs_table_t *table,
    ecs_id_t id,
    ecs_id_record_t *table_index,
    int32_t recur_depth)
{
    /* Cycle detected in IsA relationship */
    ecs_check(recur_depth < ECS_MAX_RECURSION, ECS_INVALID_PARAMETER, NULL);

    /* Table (and thus entity) does not have component, look for base */
    if (!(table->flags & EcsTableHasIsA)) {
        return NULL;
    }

    /* Exclude Name */
    if (id == ecs_pair(ecs_id(EcsIdentifier), EcsName)) {
        return NULL;
    }

    /* Table should always be in the table index for (IsA, *), otherwise the
     * HasBase flag should not have been set */
    const ecs_table_record_t *tr_isa = flecs_id_record_get_table(
        world->idr_isa_wildcard, table);
    ecs_check(tr_isa != NULL, ECS_INTERNAL_ERROR, NULL);

    ecs_type_t type = table->type;
    ecs_id_t *ids = type.array;
    int32_t i = tr_isa->column, end = tr_isa->count + tr_isa->column;
    void *ptr = NULL;

    do {
        ecs_id_t pair = ids[i ++];
        ecs_entity_t base = ecs_pair_second(world, pair);

        ecs_record_t *r = flecs_entities_get(world, base);
        ecs_assert(r != NULL, ECS_INTERNAL_ERROR, NULL);

        table = r->table;
        if (!table) {
            continue;
        }

        const ecs_table_record_t *tr = NULL;

        ecs_table_t *storage_table = table->storage_table;
        if (storage_table) {
            tr = flecs_id_record_get_table(table_index, storage_table);
        } else {
            ecs_check(!ecs_owns_id(world, base, id), 
                ECS_NOT_A_COMPONENT, NULL);
        }

        if (!tr) {
            ptr = flecs_get_base_component(world, table, id, table_index, 
                recur_depth + 1);
        } else {
            int32_t row = ECS_RECORD_TO_ROW(r->row);
            ptr = flecs_get_component_w_index(table, tr->column, row).ptr;
        }
    } while (!ptr && (i < end));

    return ptr;
error:
    return NULL;
}

static
void flecs_instantiate_slot(
    ecs_world_t *world,
    ecs_entity_t base,
    ecs_entity_t instance,
    ecs_entity_t slot_of,
    ecs_entity_t slot,
    ecs_entity_t child)
{
    if (base == slot_of) {
        /* Instance inherits from slot_of, add slot to instance */
        ecs_add_pair(world, instance, slot, child);
    } else {
        /* Slot is registered for other prefab, travel hierarchy
         * upwards to find instance that inherits from slot_of */
        ecs_entity_t parent = instance;
        int32_t depth = 0;
        do {
            if (ecs_has_pair(world, parent, EcsIsA, slot_of)) {
                const char *name = ecs_get_name(world, slot);
                if (name == NULL) {
                    char *slot_of_str = ecs_get_fullpath(world, slot_of);
                    ecs_throw(ECS_INVALID_OPERATION, "prefab '%s' has unnamed "
                        "slot (slots must be named)", slot_of_str);
                    ecs_os_free(slot_of_str);
                    return;
                }

                /* The 'slot' variable is currently pointing to a child (or 
                 * grandchild) of the current base. Find the original slot by
                 * looking it up under the prefab it was registered. */
                if (depth == 0) {
                    /* If the current instance is an instance of slot_of, just
                     * lookup the slot by name, which is faster than having to
                     * create a relative path. */
                    slot = ecs_lookup_child(world, slot_of, name);
                } else {
                    /* If the slot is more than one level away from the slot_of
                     * parent, use a relative path to find the slot */
                    char *path = ecs_get_path_w_sep(world, parent, child, ".",
                        NULL);
                    slot = ecs_lookup_path_w_sep(world, slot_of, path, ".", 
                        NULL, false);
                    ecs_os_free(path);
                }

                if (slot == 0) {
                    char *slot_of_str = ecs_get_fullpath(world, slot_of);
                    char *slot_str = ecs_get_fullpath(world, slot);
                    ecs_throw(ECS_INVALID_OPERATION,
                        "'%s' is not in hierarchy for slot '%s'",
                            slot_of_str, slot_str);
                    ecs_os_free(slot_of_str);
                    ecs_os_free(slot_str);
                }

                ecs_add_pair(world, parent, slot, child);
                break;
            }

            depth ++;
        } while ((parent = ecs_get_target(world, parent, EcsChildOf, 0)));
        
        if (parent == 0) {
            char *slot_of_str = ecs_get_fullpath(world, slot_of);
            char *slot_str = ecs_get_fullpath(world, slot);
            ecs_throw(ECS_INVALID_OPERATION,
                "'%s' is not in hierarchy for slot '%s'",
                    slot_of_str, slot_str);
            ecs_os_free(slot_of_str);
            ecs_os_free(slot_str);
        }
    }

error:
    return;
}

static
ecs_table_t* flecs_find_table_add(
    ecs_world_t *world,
    ecs_table_t *table,
    ecs_id_t id,
    ecs_table_diff_builder_t *diff)
{
    ecs_table_diff_t temp_diff = ECS_TABLE_DIFF_INIT;
    table = flecs_table_traverse_add(world, table, &id, &temp_diff);
    ecs_check(table != NULL, ECS_INVALID_PARAMETER, NULL);
    flecs_table_diff_build_append_table(world, diff, &temp_diff);
    return table;
error:
    return NULL;
}

static
ecs_table_t* flecs_find_table_remove(
    ecs_world_t *world,
    ecs_table_t *table,
    ecs_id_t id,
    ecs_table_diff_builder_t *diff)
{
    ecs_table_diff_t temp_diff = ECS_TABLE_DIFF_INIT;
    table = flecs_table_traverse_remove(world, table, &id, &temp_diff);
    ecs_check(table != NULL, ECS_INVALID_PARAMETER, NULL);
    flecs_table_diff_build_append_table(world, diff, &temp_diff);
    return table;
error:
    return NULL;
}

static
void flecs_instantiate_children(
    ecs_world_t *world,
    ecs_entity_t base,
    ecs_table_t *table,
    int32_t row,
    int32_t count,
    ecs_table_t *child_table)
{
    if (!ecs_table_count(child_table)) {
        return;
    }

    ecs_type_t type = child_table->type;
    ecs_data_t *child_data = &child_table->data;

    ecs_entity_t slot_of = 0;
    ecs_entity_t *ids = type.array;
    int32_t type_count = type.count;

    /* Instantiate child table for each instance */

    /* Create component array for creating the table */
    ecs_type_t components = {
        .array = ecs_os_alloca_n(ecs_entity_t, type_count + 1)
    };

    void **component_data = ecs_os_alloca_n(void*, type_count + 1);

    /* Copy in component identifiers. Find the base index in the component
     * array, since we'll need this to replace the base with the instance id */
    int j, i, childof_base_index = -1, pos = 0;
    for (i = 0; i < type_count; i ++) {
        ecs_id_t id = ids[i];

        /* If id has DontInherit flag don't inherit it, except for the name
         * and ChildOf pairs. The name is preserved so applications can lookup
         * the instantiated children by name. The ChildOf pair is replaced later
         * with the instance parent. */
        if ((id != ecs_pair(ecs_id(EcsIdentifier), EcsName)) &&
            ECS_PAIR_FIRST(id) != EcsChildOf) 
        {
            if (id == EcsUnion) {
                /* This should eventually be handled by the DontInherit property
                 * but right now there is no way to selectively apply it to
                 * EcsUnion itself: it would also apply to (Union, *) pairs,
                 * which would make all union relationships uninheritable. 
                 * 
                 * The reason this is explicitly skipped is so that slot 
                 * instances don't all end up with the Union property. */
                continue;
            }
            ecs_table_record_t *tr = &child_table->_->records[i];
            ecs_id_record_t *idr = (ecs_id_record_t*)tr->hdr.cache;
            if (idr->flags & EcsIdDontInherit) {
                continue;
            }
        }

        /* If child is a slot, keep track of which parent to add it to, but
         * don't add slot relationship to child of instance. If this is a child
         * of a prefab, keep the SlotOf relationship intact. */
        if (!(table->flags & EcsTableIsPrefab)) {
            if (ECS_IS_PAIR(id) && ECS_PAIR_FIRST(id) == EcsSlotOf) {
                ecs_assert(slot_of == 0, ECS_INTERNAL_ERROR, NULL);
                slot_of = ecs_pair_second(world, id);
                continue;
            }
        }

        /* Keep track of the element that creates the ChildOf relationship with
         * the prefab parent. We need to replace this element to make sure the
         * created children point to the instance and not the prefab */ 
        if (ECS_HAS_RELATION(id, EcsChildOf) && (ECS_PAIR_SECOND(id) == base)) {
            childof_base_index = pos;
        }

        int32_t storage_index = ecs_table_type_to_storage_index(child_table, i);
        if (storage_index != -1) {
            ecs_vec_t *column = &child_data->columns[storage_index];
            component_data[pos] = ecs_vec_first(column);
        } else {
            component_data[pos] = NULL;
        }

        components.array[pos] = id;
        pos ++;
    }

    /* Table must contain children of base */
    ecs_assert(childof_base_index != -1, ECS_INTERNAL_ERROR, NULL);

    /* If children are added to a prefab, make sure they are prefabs too */
    if (table->flags & EcsTableIsPrefab) {
        components.array[pos] = EcsPrefab;
        component_data[pos] = NULL;
        pos ++;
    }

    components.count = pos;

    /* Instantiate the prefab child table for each new instance */
    ecs_entity_t *instances = ecs_vec_first(&table->data.entities);
    int32_t child_count = ecs_vec_count(&child_data->entities);
    bool has_union = child_table->flags & EcsTableHasUnion;

    for (i = row; i < count + row; i ++) {
        ecs_entity_t instance = instances[i];
        ecs_table_diff_builder_t diff = ECS_TABLE_DIFF_INIT;
        flecs_table_diff_builder_init(world, &diff);
        ecs_table_t *i_table = NULL;
 
        /* Replace ChildOf element in the component array with instance id */
        components.array[childof_base_index] = ecs_pair(EcsChildOf, instance);

        /* Find or create table */
        for (j = 0; j < components.count; j ++) {
            i_table = flecs_find_table_add(
                world, i_table, components.array[j], &diff);
        }

        ecs_assert(i_table != NULL, ECS_INTERNAL_ERROR, NULL);
        ecs_assert(i_table->type.count == components.count,
            ECS_INTERNAL_ERROR, NULL);

        /* The instance is trying to instantiate from a base that is also
         * its parent. This would cause the hierarchy to instantiate itself
         * which would cause infinite recursion. */
        ecs_entity_t *children = ecs_vec_first(&child_data->entities);

#ifdef FLECS_DEBUG
        for (j = 0; j < child_count; j ++) {
            ecs_entity_t child = children[j];        
            ecs_check(child != instance, ECS_INVALID_PARAMETER, NULL);
        }
#else
        /* Bit of boilerplate to ensure that we don't get warnings about the
         * error label not being used. */
        ecs_check(true, ECS_INVALID_OPERATION, NULL);
#endif

        /* Create children */
        int32_t child_row;
        ecs_table_diff_t table_diff;
        flecs_table_diff_build_noalloc(&diff, &table_diff);
        const ecs_entity_t *i_children = flecs_bulk_new(world, i_table, NULL, 
            &components, child_count, component_data, false, &child_row, 
            &table_diff);
        flecs_table_diff_builder_fini(world, &diff);

        /* If children have union relationships, initialize */
        if (has_union) {
            ecs_table__t *meta = child_table->_;
            ecs_assert(meta != NULL, ECS_INTERNAL_ERROR, NULL);
            ecs_assert(i_table->_ != NULL, ECS_INTERNAL_ERROR, NULL);
            int32_t u, u_count = meta->sw_count;
            for (u = 0; u < u_count; u ++) {
                ecs_switch_t *src_sw = &meta->sw_columns[i];
                ecs_switch_t *dst_sw = &i_table->_->sw_columns[i];
                ecs_vec_t *v_src_values = flecs_switch_values(src_sw);
                ecs_vec_t *v_dst_values = flecs_switch_values(dst_sw);
                uint64_t *src_values = ecs_vec_first(v_src_values);
                uint64_t *dst_values = ecs_vec_first(v_dst_values);
                for (j = 0; j < child_count; j ++) {
                    dst_values[j] = src_values[j];
                }
            }
        }

        /* If children are slots, add slot relationships to parent */
        if (slot_of) {
            for (j = 0; j < child_count; j ++) {
                ecs_entity_t child = children[j];
                ecs_entity_t i_child = i_children[j];
                flecs_instantiate_slot(world, base, instance, slot_of,
                    child, i_child);
            }
        }

        /* If prefab child table has children itself, recursively instantiate */
        for (j = 0; j < child_count; j ++) {
            ecs_entity_t child = children[j];
            flecs_instantiate(world, child, i_table, child_row + j, 1);
        }
    }   
error:
    return;    
}

void flecs_instantiate(
    ecs_world_t *world,
    ecs_entity_t base,
    ecs_table_t *table,
    int32_t row,
    int32_t count)
{
    ecs_record_t *record = flecs_entities_get_any(world, base);
    ecs_table_t *base_table = record->table;
    if (!base_table || !(base_table->flags & EcsTableIsPrefab)) {
        /* Don't instantiate children from base entities that aren't prefabs */
        return;
    }

    ecs_id_record_t *idr = flecs_id_record_get(world, ecs_childof(base));
    ecs_table_cache_iter_t it;
    if (idr && flecs_table_cache_all_iter((ecs_table_cache_t*)idr, &it)) {
        const ecs_table_record_t *tr;
        while ((tr = flecs_table_cache_next(&it, ecs_table_record_t))) {
            flecs_instantiate_children(
                world, base, table, row, count, tr->hdr.table);
        }
    }
}

static
void flecs_set_union(
    ecs_world_t *world,
    ecs_table_t *table,
    int32_t row,
    int32_t count,    
    const ecs_type_t *ids)
{
    ecs_id_t *array = ids->array;
    int32_t i, id_count = ids->count;

    for (i = 0; i < id_count; i ++) {
        ecs_id_t id = array[i];

        if (ECS_HAS_ID_FLAG(id, PAIR)) {
            ecs_id_record_t *idr = flecs_id_record_get(world, 
                ecs_pair(EcsUnion, ECS_PAIR_FIRST(id)));
            if (!idr) {
                continue;
            }

            const ecs_table_record_t *tr = flecs_id_record_get_table(
                idr, table);
            ecs_assert(tr != NULL, ECS_INTERNAL_ERROR, NULL);
            ecs_assert(table->_ != NULL, ECS_INTERNAL_ERROR, NULL);
            int32_t column = tr->column - table->_->sw_offset;
            ecs_switch_t *sw = &table->_->sw_columns[column];
            ecs_entity_t union_case = 0;
            union_case = ECS_PAIR_SECOND(id);

            int32_t r;
            for (r = 0; r < count; r ++) {
                flecs_switch_set(sw, row + r, union_case);
            }
        }
    }
}

static
void flecs_notify_on_add(
    ecs_world_t *world,
    ecs_table_t *table,
    ecs_table_t *other_table,
    int32_t row,
    int32_t count,
    const ecs_type_t *added,
    ecs_flags32_t flags)
{
    ecs_assert(added != NULL, ECS_INTERNAL_ERROR, NULL);

    if (added->count) {
        ecs_flags32_t table_flags = table->flags;

        if (table_flags & EcsTableHasUnion) {
            flecs_set_union(world, table, row, count, added);
        }

        if (table_flags & (EcsTableHasOnAdd|EcsTableHasIsA|EcsTableHasTraversable)) {
            flecs_emit(world, world, &(ecs_event_desc_t){
                .event = EcsOnAdd,
                .ids = added,
                .table = table,
                .other_table = other_table,
                .offset = row,
                .count = count,
                .observable = world,
                .flags = flags
            });
        }
    }
}

void flecs_notify_on_remove(
    ecs_world_t *world,
    ecs_table_t *table,
    ecs_table_t *other_table,
    int32_t row,
    int32_t count,
    const ecs_type_t *removed)
{
    ecs_assert(removed != NULL, ECS_INTERNAL_ERROR, NULL);
    ecs_assert(count != 0, ECS_INTERNAL_ERROR, NULL);

    if (removed->count && (table->flags & 
        (EcsTableHasOnRemove|EcsTableHasUnSet|EcsTableHasIsA|EcsTableHasTraversable))) 
    {
        flecs_emit(world, world, &(ecs_event_desc_t) {
            .event = EcsOnRemove,
            .ids = removed,
            .table = table,
            .other_table = other_table,
            .offset = row,
            .count = count,
            .observable = world
        });
    }
}

static
void flecs_update_name_index(
    ecs_world_t *world,
    ecs_table_t *src, 
    ecs_table_t *dst, 
    int32_t offset, 
    int32_t count) 
{
    ecs_assert(src != NULL, ECS_INTERNAL_ERROR, NULL);
    ecs_assert(dst != NULL, ECS_INTERNAL_ERROR, NULL);
    if (!(dst->flags & EcsTableHasName)) {
        /* If destination table doesn't have a name, we don't need to update the
         * name index. Even if the src table had a name, the on_remove hook for
         * EcsIdentifier will remove the entity from the index. */
        return;
    }

    ecs_hashmap_t *src_index = src->_->name_index;
    ecs_hashmap_t *dst_index = dst->_->name_index;
    if ((src_index == dst_index) || (!src_index && !dst_index)) {
        /* If the name index didn't change, the entity still has the same parent
         * so nothing needs to be done. */
        return;
    }

    EcsIdentifier *names = ecs_table_get_pair(world, 
        dst, EcsIdentifier, EcsName, offset);
    ecs_assert(names != NULL, ECS_INTERNAL_ERROR, NULL);

    int32_t i;
    ecs_entity_t *entities = ecs_vec_get_t(
        &dst->data.entities, ecs_entity_t, offset);
    for (i = 0; i < count; i ++) {
        ecs_entity_t e = entities[i];
        EcsIdentifier *name = &names[i];

        uint64_t index_hash = name->index_hash;
        if (index_hash) {
            flecs_name_index_remove(src_index, e, index_hash);
        }
        const char *name_str = name->value;
        if (name_str) {
            ecs_assert(name->hash != 0, ECS_INTERNAL_ERROR, NULL);

            flecs_name_index_ensure(
                dst_index, e, name_str, name->length, name->hash);
            name->index = dst_index;
        }
    }
}

static
ecs_record_t* flecs_new_entity(
    ecs_world_t *world,
    ecs_entity_t entity,
    ecs_record_t *record,
    ecs_table_t *table,
    ecs_table_diff_t *diff,
    bool ctor,
    ecs_flags32_t evt_flags)
{
    ecs_assert(record != NULL, ECS_INTERNAL_ERROR, NULL);
    int32_t row = flecs_table_append(world, table, entity, record, ctor, true);
    record->table = table;
    record->row = ECS_ROW_TO_RECORD(row, record->row & ECS_ROW_FLAGS_MASK);

    ecs_assert(ecs_vec_count(&table->data.entities) > row, 
        ECS_INTERNAL_ERROR, NULL);
    flecs_notify_on_add(world, table, NULL, row, 1, &diff->added, evt_flags);

    return record;
}

static
void flecs_move_entity(
    ecs_world_t *world,
    ecs_entity_t entity,
    ecs_record_t *record,
    ecs_table_t *dst_table,
    ecs_table_diff_t *diff,
    bool ctor,
    ecs_flags32_t evt_flags)
{
    ecs_table_t *src_table = record->table;
    int32_t src_row = ECS_RECORD_TO_ROW(record->row);

    ecs_assert(src_table != NULL, ECS_INTERNAL_ERROR, NULL);
    ecs_assert(src_table != dst_table, ECS_INTERNAL_ERROR, NULL);
    ecs_assert(src_table->type.count > 0, ECS_INTERNAL_ERROR, NULL);
    ecs_assert(src_row >= 0, ECS_INTERNAL_ERROR, NULL);
    ecs_assert(ecs_vec_count(&src_table->data.entities) > src_row, 
        ECS_INTERNAL_ERROR, NULL);
    ecs_check(ecs_is_alive(world, entity), ECS_INVALID_PARAMETER, NULL);
    ecs_assert(record != NULL, ECS_INTERNAL_ERROR, NULL);
    ecs_assert(record == flecs_entities_get(world, entity), 
        ECS_INTERNAL_ERROR, NULL);

    /* Append new row to destination table */
    int32_t dst_row = flecs_table_append(world, dst_table, entity, 
        record, false, false);

    /* Invoke remove actions for removed components */
    flecs_notify_on_remove(
        world, src_table, dst_table, src_row, 1, &diff->removed);

    /* Copy entity & components from src_table to dst_table */
    flecs_table_move(world, entity, entity, dst_table, dst_row, 
        src_table, src_row, ctor);

    /* Update entity index & delete old data after running remove actions */
    record->table = dst_table;
    record->row = ECS_ROW_TO_RECORD(dst_row, record->row & ECS_ROW_FLAGS_MASK);
    
    flecs_table_delete(world, src_table, src_row, false);
    flecs_notify_on_add(
        world, dst_table, src_table, dst_row, 1, &diff->added, evt_flags);

    flecs_update_name_index(world, src_table, dst_table, dst_row, 1);

error:
    return;
}

static
void flecs_delete_entity(
    ecs_world_t *world,
    ecs_record_t *record,
    ecs_table_diff_t *diff)
{    
    ecs_table_t *table = record->table;
    int32_t row = ECS_RECORD_TO_ROW(record->row);

    /* Invoke remove actions before deleting */
    flecs_notify_on_remove(world, table, NULL, row, 1, &diff->removed);
    flecs_table_delete(world, table, row, true);
}

/* Updating component monitors is a relatively expensive operation that only
 * happens for entities that are monitored. The approach balances the amount of
 * processing between the operation on the entity vs the amount of work that
 * needs to be done to rematch queries, as a simple brute force approach does
 * not scale when there are many tables / queries. Therefore we need to do a bit
 * of bookkeeping that is more intelligent than simply flipping a flag */
static
void flecs_update_component_monitor_w_array(
    ecs_world_t *world,
    ecs_type_t *ids)
{
    if (!ids) {
        return;
    }

    int i;
    for (i = 0; i < ids->count; i ++) {
        ecs_entity_t id = ids->array[i];
        if (ECS_HAS_ID_FLAG(id, PAIR)) {
            flecs_monitor_mark_dirty(world, 
                ecs_pair(ECS_PAIR_FIRST(id), EcsWildcard));
        }

        flecs_monitor_mark_dirty(world, id);
    }
}

static
void flecs_update_component_monitors(
    ecs_world_t *world,
    ecs_type_t *added,
    ecs_type_t *removed)
{
    flecs_update_component_monitor_w_array(world, added);
    flecs_update_component_monitor_w_array(world, removed);
}

static
void flecs_commit(
    ecs_world_t *world,
    ecs_entity_t entity,
    ecs_record_t *record,
    ecs_table_t *dst_table,   
    ecs_table_diff_t *diff,
    bool construct,
    ecs_flags32_t evt_flags)
{
    ecs_assert(!(world->flags & EcsWorldReadonly), ECS_INTERNAL_ERROR, NULL);
    flecs_journal_begin(world, EcsJournalMove, entity, 
        &diff->added, &diff->removed);
    
    ecs_table_t *src_table = NULL;
    int is_trav = 0;
    if (record) {
        src_table = record->table;
        is_trav = (record->row & EcsEntityIsTraversable) != 0;
    }

    if (src_table == dst_table) {
        /* If source and destination table are the same no action is needed *
         * However, if a component was added in the process of traversing a
         * table, this suggests that a union relationship could have changed. */
        if (src_table) {
            flecs_notify_on_add(world, src_table, src_table, 
                ECS_RECORD_TO_ROW(record->row), 1, &diff->added, evt_flags);
        }
        flecs_journal_end();
        return;
    }

    if (src_table) {
        ecs_assert(dst_table != NULL, ECS_INTERNAL_ERROR, NULL);
        flecs_table_traversable_add(dst_table, is_trav);

        if (dst_table->type.count) { 
            flecs_move_entity(world, entity, record, dst_table, diff, 
                construct, evt_flags);
        } else {
            flecs_delete_entity(world, record, diff);
            record->table = NULL;
        }

        flecs_table_traversable_add(src_table, -is_trav);
    } else {        
        flecs_table_traversable_add(dst_table, is_trav);
        if (dst_table->type.count) {
            flecs_new_entity(world, entity, record, dst_table, diff, 
                construct, evt_flags);
        }
    }

    /* If the entity is being watched, it is being monitored for changes and
     * requires rematching systems when components are added or removed. This
     * ensures that systems that rely on components from containers or prefabs
     * update the matched tables when the application adds or removes a 
     * component from, for example, a container. */
    if (is_trav) {
        flecs_update_component_monitors(world, &diff->added, &diff->removed);
    }

    if ((!src_table || !src_table->type.count) && world->range_check_enabled) {
        ecs_check(!world->info.max_id || entity <= world->info.max_id, 
            ECS_OUT_OF_RANGE, 0);
        ecs_check(entity >= world->info.min_id, 
            ECS_OUT_OF_RANGE, 0);
    } 

error:
    flecs_journal_end();
    return;
}

static
const ecs_entity_t* flecs_bulk_new(
    ecs_world_t *world,
    ecs_table_t *table,
    const ecs_entity_t *entities,
    ecs_type_t *component_ids,
    int32_t count,
    void **component_data,
    bool is_move,
    int32_t *row_out,
    ecs_table_diff_t *diff)
{
    int32_t sparse_count = 0;
    if (!entities) {
        sparse_count = flecs_entities_count(world);
        entities = flecs_entities_new_ids(world, count);
    }

    if (!table) {
        return entities;
    }

    ecs_type_t type = table->type;   
    if (!type.count) {
        return entities;        
    }

    ecs_type_t component_array = { 0 };
    if (!component_ids) {
        component_ids = &component_array;
        component_array.array = type.array;
        component_array.count = type.count;
    }

    ecs_data_t *data = &table->data;
    int32_t row = flecs_table_appendn(world, table, data, count, entities);

    /* Update entity index. */
    int i;
    ecs_record_t **records = ecs_vec_first(&data->records);
    for (i = 0; i < count; i ++) {
        ecs_record_t *r = flecs_entities_get(world, entities[i]);
        r->table = table;
        r->row = ECS_ROW_TO_RECORD(row + i, 0);
        records[row + i] = r;
    }

    flecs_defer_begin(world, &world->stages[0]);
    flecs_notify_on_add(world, table, NULL, row, count, &diff->added, 
        (component_data == NULL) ? 0 : EcsEventNoOnSet);

    if (component_data) {
        int32_t c_i;
        ecs_table_t *storage_table = table->storage_table;
        for (c_i = 0; c_i < component_ids->count; c_i ++) {
            void *src_ptr = component_data[c_i];
            if (!src_ptr) {
                continue;
            }

            /* Find component in storage type */
            ecs_entity_t id = component_ids->array[c_i];
            const ecs_table_record_t *tr = flecs_table_record_get(
                world, storage_table, id);
            ecs_assert(tr != NULL, ECS_INVALID_PARAMETER, 
                "id is not a component");
            ecs_assert(tr->count == 1, ECS_INVALID_PARAMETER,
                "ids cannot be wildcards");

            int32_t index = tr->column;
            ecs_type_info_t *ti = table->type_info[index];
            ecs_vec_t *column = &table->data.columns[index];
            int32_t size = ti->size;
            ecs_assert(size != 0, ECS_INTERNAL_ERROR, NULL);
            void *ptr = ecs_vec_get(column, size, row);

            ecs_copy_t copy;
            ecs_move_t move;
            if (is_move && (move = ti->hooks.move)) {
                move(ptr, src_ptr, count, ti);
            } else if (!is_move && (copy = ti->hooks.copy)) {
                copy(ptr, src_ptr, count, ti);
            } else {
                ecs_os_memcpy(ptr, src_ptr, size * count);
            } 
        };

        flecs_notify_on_set(world, table, row, count, NULL, true);
    }

    flecs_defer_end(world, &world->stages[0]);

    if (row_out) {
        *row_out = row;
    }

    if (sparse_count) {
        entities = flecs_entities_ids(world);
        return &entities[sparse_count];
    } else {
        return entities;
    }
}

static
void flecs_add_id_w_record(
    ecs_world_t *world,
    ecs_entity_t entity,
    ecs_record_t *record,
    ecs_id_t id,
    bool construct)
{
    ecs_assert(record != NULL, ECS_INTERNAL_ERROR, NULL);

    ecs_table_t *src_table = record->table;
    ecs_table_diff_t diff = ECS_TABLE_DIFF_INIT;
    ecs_table_t *dst_table = flecs_table_traverse_add(
        world, src_table, &id, &diff);
    flecs_commit(world, entity, record, dst_table, &diff, construct, 
        EcsEventNoOnSet); /* No OnSet, this function is only called from
                           * functions that are about to set the component. */
}

static
void flecs_add_id(
    ecs_world_t *world,
    ecs_entity_t entity,
    ecs_id_t id)
{
    ecs_stage_t *stage = flecs_stage_from_world(&world);
    if (flecs_defer_add(stage, entity, id)) {
        return;
    }

    ecs_record_t *r = flecs_entities_get(world, entity);
    ecs_assert(r != NULL, ECS_INTERNAL_ERROR, NULL);
    ecs_table_diff_t diff = ECS_TABLE_DIFF_INIT;
    ecs_table_t *src_table = r->table;
    ecs_table_t *dst_table = flecs_table_traverse_add(
        world, src_table, &id, &diff);

    flecs_commit(world, entity, r, dst_table, &diff, true, 0);

    flecs_defer_end(world, stage);
}

static
void flecs_remove_id(
    ecs_world_t *world,
    ecs_entity_t entity,
    ecs_id_t id)
{
    ecs_stage_t *stage = flecs_stage_from_world(&world);
    if (flecs_defer_remove(stage, entity, id)) {
        return;
    }

    ecs_record_t *r = flecs_entities_get(world, entity);
    ecs_assert(r != NULL, ECS_INTERNAL_ERROR, NULL);
    ecs_table_t *src_table = r->table;
    ecs_table_diff_t diff = ECS_TABLE_DIFF_INIT;
    ecs_table_t *dst_table = flecs_table_traverse_remove(
        world, src_table, &id, &diff);

    flecs_commit(world, entity, r, dst_table, &diff, true, 0);

    flecs_defer_end(world, stage);
}

static
flecs_component_ptr_t flecs_get_mut(
    ecs_world_t *world,
    ecs_entity_t entity,
    ecs_entity_t id,
    ecs_record_t *r)
{
    flecs_component_ptr_t dst = {0};

    ecs_poly_assert(world, ecs_world_t);
    ecs_check(id != 0, ECS_INVALID_PARAMETER, NULL);
    ecs_check(r != NULL, ECS_INVALID_PARAMETER, NULL);
    ecs_check((id & ECS_COMPONENT_MASK) == id || 
        ECS_HAS_ID_FLAG(id, PAIR), ECS_INVALID_PARAMETER, NULL);

    if (r->table) {
        dst = flecs_get_component_ptr(
            world, r->table, ECS_RECORD_TO_ROW(r->row), id);
        if (dst.ptr) {
            return dst;
        }
    }

    /* If entity didn't have component yet, add it */
    flecs_add_id_w_record(world, entity, r, id, true);

    /* Flush commands so the pointer we're fetching is stable */
    flecs_defer_end(world, &world->stages[0]);
    flecs_defer_begin(world, &world->stages[0]);

    ecs_assert(r->table != NULL, ECS_INTERNAL_ERROR, NULL);
    ecs_assert(r->table->storage_table != NULL, ECS_INTERNAL_ERROR, NULL);
    dst = flecs_get_component_ptr(
        world, r->table, ECS_RECORD_TO_ROW(r->row), id);
error:
    return dst;
}

void flecs_invoke_hook(
    ecs_world_t *world,
    ecs_table_t *table,
    int32_t count,
    int32_t row,
    ecs_entity_t *entities,
    void *ptr,
    ecs_id_t id,
    const ecs_type_info_t *ti,
    ecs_entity_t event,
    ecs_iter_action_t hook)
{
    ecs_assert(ti->size != 0, ECS_INVALID_PARAMETER, NULL);

    ecs_iter_t it = { .field_count = 1};
    it.entities = entities;
    
    flecs_iter_init(world, &it, flecs_iter_cache_all);
    it.world = world;
    it.real_world = world;
    it.table = table;
    it.ptrs[0] = ptr;
    it.sizes = (ecs_size_t*)&ti->size;
    it.ids[0] = id;
    it.event = event;
    it.event_id = id;
    it.ctx = ti->hooks.ctx;
    it.binding_ctx = ti->hooks.binding_ctx;
    it.count = count;
    it.offset = row;
    flecs_iter_validate(&it);
    hook(&it);
    ecs_iter_fini(&it);
}

void flecs_notify_on_set(
    ecs_world_t *world,
    ecs_table_t *table,
    int32_t row,
    int32_t count,
    ecs_type_t *ids,
    bool owned)
{
    ecs_data_t *data = &table->data;

    ecs_entity_t *entities = ecs_vec_get_t(
        &data->entities, ecs_entity_t, row);
    ecs_assert(entities != NULL, ECS_INTERNAL_ERROR, NULL);
    ecs_assert((row + count) <= ecs_vec_count(&data->entities), 
        ECS_INTERNAL_ERROR, NULL);

    ecs_type_t local_ids;
    if (!ids) {
        local_ids.array = table->storage_ids;
        local_ids.count = table->storage_count;
        ids = &local_ids;
    }

    if (owned) {
        ecs_table_t *storage_table = table->storage_table;
        int i;
        for (i = 0; i < ids->count; i ++) {
            ecs_id_t id = ids->array[i];
            const ecs_table_record_t *tr = flecs_table_record_get(world, 
                storage_table, id);
            ecs_assert(tr != NULL, ECS_INTERNAL_ERROR, NULL);
            ecs_assert(tr->count == 1, ECS_INTERNAL_ERROR, NULL);
            int32_t column = tr->column;
            const ecs_type_info_t *ti = table->type_info[column];
            ecs_iter_action_t on_set = ti->hooks.on_set;
            if (on_set) {
                ecs_vec_t *c = &table->data.columns[column];
                void *ptr = ecs_vec_get(c, ti->size, row);
                flecs_invoke_hook(world, table, count, row, entities, ptr, id, 
                    ti, EcsOnSet, on_set);
            }
        }
    }

    /* Run OnSet notifications */
    if (table->flags & EcsTableHasOnSet && ids->count) {
        flecs_emit(world, world, &(ecs_event_desc_t) {
            .event = EcsOnSet,
            .ids = ids,
            .table = table,
            .offset = row,
            .count = count,
            .observable = world
        });
    }
}

void flecs_record_add_flag(
    ecs_record_t *record,
    uint32_t flag)
{
    if (flag == EcsEntityIsTraversable) {
        if (!(record->row & flag)) {
            ecs_table_t *table = record->table;
            if (table) {
                flecs_table_traversable_add(table, 1);
            }
        }
    }
    record->row |= flag;
}

void flecs_add_flag(
    ecs_world_t *world,
    ecs_entity_t entity,
    uint32_t flag)
{
    ecs_record_t *record = flecs_entities_get_any(world, entity);
    ecs_assert(record != NULL, ECS_INTERNAL_ERROR, NULL);
    flecs_record_add_flag(record, flag);
}

/* -- Public functions -- */

bool ecs_commit(
    ecs_world_t *world,
    ecs_entity_t entity,
    ecs_record_t *record,
    ecs_table_t *table,
    const ecs_type_t *added,
    const ecs_type_t *removed)
{
    ecs_check(world != NULL, ECS_INVALID_PARAMETER, NULL);

    ecs_table_t *src_table = NULL;
    if (!record) {
        record = flecs_entities_get(world, entity);
        src_table = record->table;
    }

    ecs_table_diff_t diff = ECS_TABLE_DIFF_INIT;

    if (added) {
        diff.added = *added;
    }
    if (removed) {
        diff.added = *removed;
    }
    
    flecs_commit(world, entity, record, table, &diff, true, 0);

    return src_table != table;
error:
    return false;
}

ecs_entity_t ecs_set_with(
    ecs_world_t *world,
    ecs_id_t id)
{
    ecs_check(world != NULL, ECS_INVALID_PARAMETER, NULL);
    ecs_stage_t *stage = flecs_stage_from_world(&world);
    ecs_id_t prev = stage->with;
    stage->with = id;
    return prev;
error:
    return 0;
}

ecs_id_t ecs_get_with(
    const ecs_world_t *world)
{
    ecs_check(world != NULL, ECS_INVALID_PARAMETER, NULL);
    const ecs_stage_t *stage = flecs_stage_from_readonly_world(world);
    return stage->with;
error:
    return 0;
}

ecs_entity_t ecs_new_id(
    ecs_world_t *world)
{
    ecs_check(world != NULL, ECS_INVALID_PARAMETER, NULL);

    const ecs_stage_t *stage = flecs_stage_from_readonly_world(world);

    /* It is possible that the world passed to this function is a stage, so
     * make sure we have the actual world. Cast away const since this is one of
     * the few functions that may modify the world while it is in readonly mode,
     * since it is thread safe (uses atomic inc when in threading mode) */
    ecs_world_t *unsafe_world = (ecs_world_t*)ecs_get_world(world);

    ecs_entity_t entity;
    if (stage->async || (unsafe_world->flags & EcsWorldMultiThreaded)) {
        /* When using an async stage or world is in multithreading mode, make
         * sure OS API has threading functions initialized */
        ecs_assert(ecs_os_has_threading(), ECS_INVALID_OPERATION, NULL);

        /* Can't atomically increase number above max int */
        ecs_assert(flecs_entities_max_id(unsafe_world) < UINT_MAX, 
            ECS_INVALID_OPERATION, NULL);
        entity = (ecs_entity_t)ecs_os_ainc(
            (int32_t*)&flecs_entities_max_id(unsafe_world));
    } else {
        entity = flecs_entities_new_id(unsafe_world);
    }

    ecs_assert(!unsafe_world->info.max_id || 
        ecs_entity_t_lo(entity) <= unsafe_world->info.max_id, 
        ECS_OUT_OF_RANGE, NULL);

    flecs_journal(world, EcsJournalNew, entity, 0, 0);

    return entity;
error:
    return 0;
}

ecs_entity_t ecs_new_low_id(
    ecs_world_t *world)
{
    ecs_check(world != NULL, ECS_INVALID_PARAMETER, NULL);

    /* It is possible that the world passed to this function is a stage, so
     * make sure we have the actual world. Cast away const since this is one of
     * the few functions that may modify the world while it is in readonly mode,
     * but only if single threaded. */
    ecs_world_t *unsafe_world = (ecs_world_t*)ecs_get_world(world);
    if (unsafe_world->flags & EcsWorldReadonly) {
        /* Can't issue new comp id while iterating when in multithreaded mode */
        ecs_check(ecs_get_stage_count(world) <= 1,
            ECS_INVALID_WHILE_READONLY, NULL);
    }

    ecs_entity_t id = 0;
    if (unsafe_world->info.last_component_id < FLECS_HI_COMPONENT_ID) {
        do {
            id = unsafe_world->info.last_component_id ++;
        } while (ecs_exists(unsafe_world, id) && id <= FLECS_HI_COMPONENT_ID);        
    }

    if (!id || id >= FLECS_HI_COMPONENT_ID) {
        /* If the low component ids are depleted, return a regular entity id */
        id = ecs_new_id(unsafe_world);
    } else {
        flecs_entities_ensure(world, id);
    }

    ecs_assert(ecs_get_type(world, id) == NULL, ECS_INTERNAL_ERROR, NULL);

    return id;
error: 
    return 0;
}

ecs_entity_t ecs_new_w_id(
    ecs_world_t *world,
    ecs_id_t id)
{
    ecs_check(world != NULL, ECS_INVALID_PARAMETER, NULL);
    ecs_check(!id || ecs_id_is_valid(world, id), ECS_INVALID_PARAMETER, NULL);

    ecs_stage_t *stage = flecs_stage_from_world(&world);    
    ecs_entity_t entity = ecs_new_id(world);

    ecs_id_t ids[3];
    ecs_type_t to_add = { .array = ids, .count = 0 };

    if (id) {
        ids[to_add.count ++] = id;
    }

    ecs_id_t with = stage->with;
    if (with) {
        ids[to_add.count ++] = with;
    }

    ecs_entity_t scope = stage->scope;
    if (scope) {
        if (!id || !ECS_HAS_RELATION(id, EcsChildOf)) {
            ids[to_add.count ++] = ecs_pair(EcsChildOf, scope);
        }
    }
    if (to_add.count) {
        if (flecs_defer_add(stage, entity, to_add.array[0])) {
            int i;
            for (i = 1; i < to_add.count; i ++) {
                flecs_defer_add(stage, entity, to_add.array[i]);
            }
            return entity;
        }

        int32_t i, count = to_add.count;
        ecs_table_t *table = &world->store.root;
        
        ecs_table_diff_builder_t diff = ECS_TABLE_DIFF_INIT;
        flecs_table_diff_builder_init(world, &diff);
        for (i = 0; i < count; i ++) {
            table = flecs_find_table_add(
                world, table, to_add.array[i], &diff);
        }

        ecs_table_diff_t table_diff;
        flecs_table_diff_build_noalloc(&diff, &table_diff);
        ecs_record_t *r = flecs_entities_get(world, entity);
        flecs_new_entity(world, entity, r, table, &table_diff, true, true);
        flecs_table_diff_builder_fini(world, &diff);
    } else {
        if (flecs_defer_cmd(stage)) {
            return entity;
        }

        flecs_entities_ensure(world, entity);
    }
    flecs_defer_end(world, stage);

    return entity;
error:
    return 0;
}

ecs_entity_t ecs_new_w_table(
    ecs_world_t *world,
    ecs_table_t *table)
{
    ecs_check(world != NULL, ECS_INVALID_PARAMETER, NULL);

    flecs_stage_from_world(&world);    
    ecs_entity_t entity = ecs_new_id(world);
    ecs_record_t *r = flecs_entities_get(world, entity);

    ecs_table_diff_t table_diff = { .added = table->type };
    flecs_new_entity(world, entity, r, table, &table_diff, true, true);
    return entity;
error:
    return 0;
}

#ifdef FLECS_PARSER

/* Traverse table graph by either adding or removing identifiers parsed from the
 * passed in expression. */
static
ecs_table_t *flecs_traverse_from_expr(
    ecs_world_t *world,
    ecs_table_t *table,
    const char *name,
    const char *expr,
    ecs_table_diff_builder_t *diff,
    bool replace_and,
    bool *error)
{
    const char *ptr = expr;
    if (ptr) {
        ecs_term_t term = {0};
        while (ptr[0] && (ptr = ecs_parse_term(world, name, expr, ptr, &term))){
            if (!ecs_term_is_initialized(&term)) {
                break;
            }

            if (!(term.first.flags & (EcsSelf|EcsUp))) {
                term.first.flags = EcsSelf;
            }
            if (!(term.second.flags & (EcsSelf|EcsUp))) {
                term.second.flags = EcsSelf;
            }
            if (!(term.src.flags & (EcsSelf|EcsUp))) {
                term.src.flags = EcsSelf;
            }

            if (ecs_term_finalize(world, &term)) {
                ecs_term_fini(&term);
                if (error) {
                    *error = true;
                }
                return NULL;
            }

            if (!ecs_id_is_valid(world, term.id)) {
                ecs_term_fini(&term);
                ecs_parser_error(name, expr, (ptr - expr), 
                    "invalid term for add expression");
                return NULL;
            }

            if (term.oper == EcsAnd || !replace_and) {
                /* Regular AND expression */
                table = flecs_find_table_add(world, table, term.id, diff);
            }

            ecs_term_fini(&term);
        }

        if (!ptr) {
            if (error) {
                *error = true;
            }
            return NULL;
        }
    }

    return table;
}

/* Add/remove components based on the parsed expression. This operation is 
 * slower than flecs_traverse_from_expr, but safe to use from a deferred context. */
static
void flecs_defer_from_expr(
    ecs_world_t *world,
    ecs_entity_t entity,
    const char *name,
    const char *expr,
    bool is_add,
    bool replace_and)
{
    const char *ptr = expr;
    if (ptr) {
        ecs_term_t term = {0};
        while (ptr[0] && (ptr = ecs_parse_term(world, name, expr, ptr, &term))){
            if (!ecs_term_is_initialized(&term)) {
                break;
            }

            if (ecs_term_finalize(world, &term)) {
                return;
            }

            if (!ecs_id_is_valid(world, term.id)) {
                ecs_term_fini(&term);
                ecs_parser_error(name, expr, (ptr - expr), 
                    "invalid term for add expression");
                return;
            }

            if (term.oper == EcsAnd || !replace_and) {
                /* Regular AND expression */
                if (is_add) {
                    ecs_add_id(world, entity, term.id);
                } else {
                    ecs_remove_id(world, entity, term.id);
                }
            }

            ecs_term_fini(&term);
        }
    }
}
#endif

/* If operation is not deferred, add components by finding the target
 * table and moving the entity towards it. */
static 
int flecs_traverse_add(
    ecs_world_t *world,
    ecs_entity_t result,
    const char *name,
    const ecs_entity_desc_t *desc,
    ecs_entity_t scope,
    ecs_id_t with,
    bool flecs_new_entity,
    bool name_assigned)
{
    const char *sep = desc->sep;
    const char *root_sep = desc->root_sep;
    ecs_table_diff_builder_t diff = ECS_TABLE_DIFF_INIT;
    flecs_table_diff_builder_init(world, &diff);

    /* Find existing table */
    ecs_table_t *src_table = NULL, *table = NULL;
    ecs_record_t *r = flecs_entities_get(world, result);
    table = r->table;

    /* If a name is provided but not yet assigned, add the Name component */
    if (name && !name_assigned) {
        table = flecs_find_table_add(world, table, 
            ecs_pair(ecs_id(EcsIdentifier), EcsName), &diff);
    }

    /* Add components from the 'add' id array */
    int32_t i = 0;
    ecs_id_t id;
    const ecs_id_t *ids = desc->add;
    while ((i < FLECS_ID_DESC_MAX) && (id = ids[i ++])) {
        bool should_add = true;
        if (ECS_HAS_ID_FLAG(id, PAIR) && ECS_PAIR_FIRST(id) == EcsChildOf) {
            scope = ECS_PAIR_SECOND(id);
            if ((!desc->id && desc->name) || (name && !name_assigned)) {
                /* If name is added to entity, pass scope to add_path instead
                 * of adding it to the table. The provided name may have nested
                 * elements, in which case the parent provided here is not the
                 * parent the entity will end up with. */
                should_add = false;
            }
        }
        if (should_add) {
            table = flecs_find_table_add(world, table, id, &diff);
        }
    }

    /* Find destination table */
    /* If this is a new entity without a name, add the scope. If a name is
     * provided, the scope will be added by the add_path_w_sep function */
    if (flecs_new_entity) {
        if (flecs_new_entity && scope && !name && !name_assigned) {
            table = flecs_find_table_add(
                world, table, ecs_pair(EcsChildOf, scope), &diff);
        }
        if (with) {
            table = flecs_find_table_add(world, table, with, &diff);
        }
    }

    /* Add components from the 'add_expr' expression */
    if (desc->add_expr && ecs_os_strcmp(desc->add_expr, "0")) {
#ifdef FLECS_PARSER
        bool error = false;
        table = flecs_traverse_from_expr(
            world, table, name, desc->add_expr, &diff, true, &error);
        if (error) {
            flecs_table_diff_builder_fini(world, &diff);
            return -1;
        }
#else
        ecs_abort(ECS_UNSUPPORTED, "parser addon is not available");
#endif
    }

    /* Commit entity to destination table */
    if (src_table != table) {
        flecs_defer_begin(world, &world->stages[0]);
        ecs_table_diff_t table_diff;
        flecs_table_diff_build_noalloc(&diff, &table_diff);
        flecs_commit(world, result, r, table, &table_diff, true, 0);
        flecs_table_diff_builder_fini(world, &diff);
        flecs_defer_end(world, &world->stages[0]);
    }

    /* Set name */
    if (name && !name_assigned) {
        ecs_add_path_w_sep(world, result, scope, name, sep, root_sep);
        ecs_assert(ecs_get_name(world, result) != NULL,
            ECS_INTERNAL_ERROR, NULL);
    }

    if (desc->symbol && desc->symbol[0]) {
        const char *sym = ecs_get_symbol(world, result);
        if (sym) {
            ecs_assert(!ecs_os_strcmp(desc->symbol, sym),
                ECS_INCONSISTENT_NAME, desc->symbol);
        } else {
            ecs_set_symbol(world, result, desc->symbol);
        }
    }

    flecs_table_diff_builder_fini(world, &diff);
    return 0;
}

/* When in deferred mode, we need to add/remove components one by one using
 * the regular operations. */
static 
void flecs_deferred_add_remove(
    ecs_world_t *world,
    ecs_entity_t entity,
    const char *name,
    const ecs_entity_desc_t *desc,
    ecs_entity_t scope,
    ecs_id_t with,
    bool flecs_new_entity,
    bool name_assigned)
{
    const char *sep = desc->sep;
    const char *root_sep = desc->root_sep;

    /* If this is a new entity without a name, add the scope. If a name is
     * provided, the scope will be added by the add_path_w_sep function */
    if (flecs_new_entity) {
        if (flecs_new_entity && scope && !name && !name_assigned) {
            ecs_add_id(world, entity, ecs_pair(EcsChildOf, scope));
        }

        if (with) {
            ecs_add_id(world, entity, with);
        }
    }

    /* Add components from the 'add' id array */
    int32_t i = 0;
    ecs_id_t id;
    const ecs_id_t *ids = desc->add;
    while ((i < FLECS_ID_DESC_MAX) && (id = ids[i ++])) {
        bool defer = true;
        if (ECS_HAS_ID_FLAG(id, PAIR) && ECS_PAIR_FIRST(id) == EcsChildOf) {
            scope = ECS_PAIR_SECOND(id);
            if (name && (!desc->id || !name_assigned)) {
                /* New named entities are created by temporarily going out of
                 * readonly mode to ensure no duplicates are created. */
                defer = false;
            }
        }
        if (defer) {
            ecs_add_id(world, entity, id);
        }
    }

    /* Add components from the 'add_expr' expression */
    if (desc->add_expr) {
#ifdef FLECS_PARSER
        flecs_defer_from_expr(world, entity, name, desc->add_expr, true, true);
#else
        ecs_abort(ECS_UNSUPPORTED, "parser addon is not available");
#endif
    }

    int32_t thread_count = ecs_get_stage_count(world);

    /* Set name */
    if (name && !name_assigned) {
        ecs_add_path_w_sep(world, entity, scope, name, sep, root_sep);
    }

    /* Set symbol */
    if (desc->symbol) {
        const char *sym = ecs_get_symbol(world, entity);
        if (!sym || ecs_os_strcmp(sym, desc->symbol)) {
            if (thread_count <= 1) { /* See above */
                ecs_suspend_readonly_state_t state;
                ecs_world_t *real_world = flecs_suspend_readonly(world, &state);
                ecs_set_symbol(world, entity, desc->symbol);
                flecs_resume_readonly(real_world, &state);
            } else {
                ecs_set_symbol(world, entity, desc->symbol);
            }
        }
    }
}

ecs_entity_t ecs_entity_init(
    ecs_world_t *world,
    const ecs_entity_desc_t *desc)
{
    ecs_check(world != NULL, ECS_INVALID_PARAMETER, NULL);
    ecs_check(desc != NULL, ECS_INVALID_PARAMETER, NULL);
    ecs_check(desc->_canary == 0, ECS_INVALID_PARAMETER, NULL);

    ecs_stage_t *stage = flecs_stage_from_world(&world);
    ecs_entity_t scope = stage->scope;
    ecs_id_t with = ecs_get_with(world);
    ecs_entity_t result = desc->id;

    const char *name = desc->name;
    const char *sep = desc->sep;
    if (!sep) {
        sep = ".";
    }

    if (name) {
        if (!name[0]) {
            name = NULL;
        } else if (flecs_name_is_id(name)){
            ecs_entity_t id = flecs_name_to_id(world, name);
            if (!id) {
                return 0;
            }
            if (result && (id != result)) {
                ecs_err("name id conflicts with provided id");
                return 0;
            }
            name = NULL;
            result = id;
        }
    }

    const char *root_sep = desc->root_sep;
    bool flecs_new_entity = false;
    bool name_assigned = false;

    /* Remove optional prefix from name. Entity names can be derived from 
     * language identifiers, such as components (typenames) and systems
     * function names). Because C does not have namespaces, such identifiers
     * often encode the namespace as a prefix.
     * To ensure interoperability between C and C++ (and potentially other 
     * languages with namespacing) the entity must be stored without this prefix
     * and with the proper namespace, which is what the name_prefix is for */
    const char *prefix = world->info.name_prefix;
    if (name && prefix) {
        ecs_size_t len = ecs_os_strlen(prefix);
        if (!ecs_os_strncmp(name, prefix, len) && 
           (isupper(name[len]) || name[len] == '_')) 
        {
            if (name[len] == '_') {
                name = name + len + 1;
            } else {
                name = name + len;
            }
        }
    }

    /* Find or create entity */
    if (!result) {
        if (name) {
            /* If add array contains a ChildOf pair, use it as scope instead */
            const ecs_id_t *ids = desc->add;
            ecs_id_t id;
            int32_t i = 0;
            while ((i < FLECS_ID_DESC_MAX) && (id = ids[i ++])) {
                if (ECS_HAS_ID_FLAG(id, PAIR) && 
                    (ECS_PAIR_FIRST(id) == EcsChildOf))
                {
                    scope = ECS_PAIR_SECOND(id);
                }
            }

            result = ecs_lookup_path_w_sep(
                world, scope, name, sep, root_sep, false);
            if (result) {
                name_assigned = true;
            }
        }

        if (!result) {
            if (desc->use_low_id) {
                result = ecs_new_low_id(world);
            } else {
                result = ecs_new_id(world);
            }
            flecs_new_entity = true;
            ecs_assert(ecs_get_type(world, result) == NULL,
                ECS_INTERNAL_ERROR, NULL);
        }
    } else {
        /* Make sure provided id is either alive or revivable */
        ecs_ensure(world, result);

        name_assigned = ecs_has_pair(
            world, result, ecs_id(EcsIdentifier), EcsName);
        if (name && name_assigned) {
            /* If entity has name, verify that name matches. The name provided
             * to the function could either have been relative to the current
             * scope, or fully qualified. */
            char *path;
            ecs_size_t root_sep_len = root_sep ? ecs_os_strlen(root_sep) : 0;
            if (root_sep && !ecs_os_strncmp(name, root_sep, root_sep_len)) {
                /* Fully qualified name was provided, so make sure to
                 * compare with fully qualified name */
                path = ecs_get_path_w_sep(world, 0, result, sep, root_sep);
            } else {
                /* Relative name was provided, so make sure to compare with
                 * relative name */
                path = ecs_get_path_w_sep(world, scope, result, sep, "");
            }
            if (path) {
                if (ecs_os_strcmp(path, name)) {
                    /* Mismatching name */
                    ecs_err("existing entity '%s' is initialized with "
                        "conflicting name '%s'", path, name);
                    ecs_os_free(path);
                    return 0;
                }
                ecs_os_free(path);
            }
        }
    }

    ecs_assert(name_assigned == ecs_has_pair(
        world, result, ecs_id(EcsIdentifier), EcsName),
            ECS_INTERNAL_ERROR, NULL);

    if (stage->defer) {
        flecs_deferred_add_remove((ecs_world_t*)stage, result, name, desc, 
            scope, with, flecs_new_entity, name_assigned);
    } else {
        if (flecs_traverse_add(world, result, name, desc,
            scope, with, flecs_new_entity, name_assigned)) 
        {
            return 0;
        }
    }

    return result;
error:
    return 0;
}

const ecs_entity_t* ecs_bulk_init(
    ecs_world_t *world,
    const ecs_bulk_desc_t *desc)
{
    ecs_poly_assert(world, ecs_world_t);
    ecs_assert(!(world->flags & EcsWorldReadonly), ECS_INTERNAL_ERROR, NULL);
    ecs_check(desc != NULL, ECS_INVALID_PARAMETER, NULL);
    ecs_check(desc->_canary == 0, ECS_INVALID_PARAMETER, NULL);

    const ecs_entity_t *entities = desc->entities;
    int32_t count = desc->count;

    int32_t sparse_count = 0;
    if (!entities) {
        sparse_count = flecs_entities_count(world);
        entities = flecs_entities_new_ids(world, count);
        ecs_assert(entities != NULL, ECS_INTERNAL_ERROR, NULL);
    } else {
        int i;
        for (i = 0; i < count; i ++) {
            ecs_ensure(world, entities[i]);
        }
    }

    ecs_type_t ids;
    ecs_table_t *table = desc->table;
    if (!table) {
        ecs_table_diff_builder_t diff = ECS_TABLE_DIFF_INIT;
        flecs_table_diff_builder_init(world, &diff);

        int32_t i = 0;
        ecs_id_t id;
        while ((id = desc->ids[i])) {
            table = flecs_find_table_add(world, table, id, &diff);
            i ++;
        }

        ids.array = (ecs_id_t*)desc->ids;
        ids.count = i;

        ecs_table_diff_t table_diff;
        flecs_table_diff_build_noalloc(&diff, &table_diff);
        flecs_bulk_new(world, table, entities, &ids, count, desc->data, true, NULL, 
            &table_diff);
        flecs_table_diff_builder_fini(world, &diff);
    } else {
        ecs_table_diff_t diff = {
            .added.array = table->type.array,
            .added.count = table->type.count
        };
        ids = (ecs_type_t){.array = diff.added.array, .count = diff.added.count};
        flecs_bulk_new(world, table, entities, &ids, count, desc->data, true, NULL, 
            &diff);
    }

    if (!sparse_count) {
        return entities;
    } else {
        /* Refetch entity ids, in case the underlying array was reallocated */
        entities = flecs_entities_ids(world);
        return &entities[sparse_count];
    }
error:
    return NULL;
}

static
void flecs_check_component(
    ecs_world_t *world,
    ecs_entity_t result,
    const EcsComponent *ptr,
    ecs_size_t size,
    ecs_size_t alignment)
{
    if (ptr->size != size) {
        char *path = ecs_get_fullpath(world, result);
        ecs_abort(ECS_INVALID_COMPONENT_SIZE, path);
        ecs_os_free(path);
    }
    if (ptr->alignment != alignment) {
        char *path = ecs_get_fullpath(world, result);
        ecs_abort(ECS_INVALID_COMPONENT_ALIGNMENT, path);
        ecs_os_free(path);
    }
}

ecs_entity_t ecs_component_init(
    ecs_world_t *world,
    const ecs_component_desc_t *desc)
{
    ecs_check(world != NULL, ECS_INVALID_PARAMETER, NULL);
    ecs_check(desc != NULL, ECS_INVALID_PARAMETER, NULL);
    ecs_check(desc->_canary == 0, ECS_INVALID_PARAMETER, NULL);

    /* If existing entity is provided, check if it is already registered as a
     * component and matches the size/alignment. This can prevent having to
     * suspend readonly mode, and increases the number of scenarios in which
     * this function can be called in multithreaded mode. */
    ecs_entity_t result = desc->entity;
    if (result && ecs_is_alive(world, result)) {
        const EcsComponent *const_ptr = ecs_get(world, result, EcsComponent);
        if (const_ptr) {
            flecs_check_component(world, result, const_ptr,
                desc->type.size, desc->type.alignment);
            return result;
        }
    }

    ecs_suspend_readonly_state_t readonly_state;
    world = flecs_suspend_readonly(world, &readonly_state);

    bool new_component = true;
    if (!result) {
        result = ecs_new_low_id(world);
    } else {
        ecs_ensure(world, result);
        new_component = ecs_has(world, result, EcsComponent);
    }

    EcsComponent *ptr = ecs_get_mut(world, result, EcsComponent);
    if (!ptr->size) {
        ecs_assert(ptr->alignment == 0, ECS_INTERNAL_ERROR, NULL);
        ptr->size = desc->type.size;
        ptr->alignment = desc->type.alignment;
        if (!new_component || ptr->size != desc->type.size) {
            if (!ptr->size) {
                ecs_trace("#[green]tag#[reset] %s created", 
                    ecs_get_name(world, result));
            } else {
                ecs_trace("#[green]component#[reset] %s created", 
                    ecs_get_name(world, result));
            }
        }
    } else {
        flecs_check_component(world, result, ptr,
            desc->type.size, desc->type.alignment);
    }

    ecs_modified(world, result, EcsComponent);

    if (desc->type.size && 
        !ecs_id_in_use(world, result) && 
        !ecs_id_in_use(world, ecs_pair(result, EcsWildcard)))
    {
        ecs_set_hooks_id(world, result, &desc->type.hooks);
    }

    if (result >= world->info.last_component_id && result < FLECS_HI_COMPONENT_ID) {
        world->info.last_component_id = result + 1;
    }

    /* Ensure components cannot be deleted */
    ecs_add_pair(world, result, EcsOnDelete, EcsPanic);

    flecs_resume_readonly(world, &readonly_state);
    
    ecs_assert(result != 0, ECS_INTERNAL_ERROR, NULL);
    ecs_assert(ecs_has(world, result, EcsComponent), ECS_INTERNAL_ERROR, NULL);

    return result;
error:
    return 0;
}

const ecs_entity_t* ecs_bulk_new_w_id(
    ecs_world_t *world,
    ecs_id_t id,
    int32_t count)
{
    ecs_check(world != NULL, ECS_INVALID_PARAMETER, NULL);
    ecs_stage_t *stage = flecs_stage_from_world(&world);

    const ecs_entity_t *ids;
    if (flecs_defer_bulk_new(world, stage, count, id, &ids)) {
        return ids;
    }

    ecs_table_t *table = &world->store.root;
    ecs_table_diff_builder_t diff = ECS_TABLE_DIFF_INIT;
    flecs_table_diff_builder_init(world, &diff);
    
    if (id) {
        table = flecs_find_table_add(world, table, id, &diff);
    }

    ecs_table_diff_t td;
    flecs_table_diff_build_noalloc(&diff, &td);
    ids = flecs_bulk_new(world, table, NULL, NULL, count, NULL, false, NULL, &td);
    flecs_table_diff_builder_fini(world, &diff);
    flecs_defer_end(world, stage);

    return ids;
error:
    return NULL;
}

void ecs_clear(
    ecs_world_t *world,
    ecs_entity_t entity)
{
    ecs_check(world != NULL, ECS_INVALID_PARAMETER, NULL);
    ecs_check(ecs_is_valid(world, entity), ECS_INVALID_PARAMETER, NULL);

    ecs_stage_t *stage = flecs_stage_from_world(&world);
    if (flecs_defer_clear(stage, entity)) {
        return;
    }

    ecs_record_t *r = flecs_entities_get(world, entity);
    ecs_assert(r != NULL, ECS_INTERNAL_ERROR, NULL);

    ecs_table_t *table = r->table;
    if (table) {
        ecs_table_diff_t diff = {
            .removed = table->type
        };

        flecs_delete_entity(world, r, &diff);
        r->table = NULL;

        if (r->row & EcsEntityIsTraversable) {
            flecs_table_traversable_add(table, -1);
        }
    }    

    flecs_defer_end(world, stage);
error:
    return;
}

static
void flecs_throw_invalid_delete(
    ecs_world_t *world,
    ecs_id_t id)
{
    char *id_str = NULL;
    if (!(world->flags & EcsWorldQuit)) {
        id_str = ecs_id_str(world, id);
        ecs_throw(ECS_CONSTRAINT_VIOLATED, id_str);
    }
error:
    ecs_os_free(id_str);
}

static
void flecs_marked_id_push(
    ecs_world_t *world,
    ecs_id_record_t* idr,
    ecs_entity_t action,
    bool delete_id)
{
    ecs_marked_id_t *m = ecs_vec_append_t(&world->allocator,
        &world->store.marked_ids, ecs_marked_id_t);

    m->idr = idr;
    m->id = idr->id;
    m->action = action;
    m->delete_id = delete_id;

    flecs_id_record_claim(world, idr);
}

static
void flecs_id_mark_for_delete(
    ecs_world_t *world,
    ecs_id_record_t *idr,
    ecs_entity_t action,
    bool delete_id);

static
void flecs_targets_mark_for_delete(
    ecs_world_t *world,
    ecs_table_t *table)
{
    ecs_id_record_t *idr;
    ecs_entity_t *entities = ecs_vec_first(&table->data.entities);
    ecs_record_t **records = ecs_vec_first(&table->data.records);
    int32_t i, count = ecs_vec_count(&table->data.entities);
    for (i = 0; i < count; i ++) {
        ecs_record_t *r = records[i];
        if (!r) {
            continue;
        }

        /* If entity is not used as id or as relationship target, there won't
         * be any tables with a reference to it. */
        ecs_flags32_t flags = r->row & ECS_ROW_FLAGS_MASK;
        if (!(flags & (EcsEntityIsId|EcsEntityIsTarget))) {
            continue;
        }

        ecs_entity_t e = entities[i];
        if (flags & EcsEntityIsId) {
            if ((idr = flecs_id_record_get(world, e))) {
                flecs_id_mark_for_delete(world, idr, 
                    ECS_ID_ON_DELETE(idr->flags), true);
            }
            if ((idr = flecs_id_record_get(world, ecs_pair(e, EcsWildcard)))) {
                flecs_id_mark_for_delete(world, idr, 
                    ECS_ID_ON_DELETE(idr->flags), true);
            }
        }
        if (flags & EcsEntityIsTarget) {
            if ((idr = flecs_id_record_get(world, ecs_pair(EcsWildcard, e)))) {
                flecs_id_mark_for_delete(world, idr, 
                    ECS_ID_ON_DELETE_OBJECT(idr->flags), true);
            }
            if ((idr = flecs_id_record_get(world, ecs_pair(EcsFlag, e)))) {
                flecs_id_mark_for_delete(world, idr, 
                    ECS_ID_ON_DELETE_OBJECT(idr->flags), true);
            }
        }
    }
}

static
bool flecs_id_is_delete_target(
    ecs_id_t id,
    ecs_entity_t action)
{
    if (!action && ecs_id_is_pair(id) && ECS_PAIR_FIRST(id) == EcsWildcard) {
        /* If no explicit delete action is provided, and the id we're deleting
         * has the form (*, Target), use OnDeleteTarget action */
        return true;
    }
    return false;
}

static
ecs_entity_t flecs_get_delete_action(
    ecs_table_t *table,
    ecs_table_record_t *tr,
    ecs_entity_t action,
    bool delete_target)
{
    ecs_entity_t result = action;
    if (!result && delete_target) {
        /* If action is not specified and we're deleting a relationship target,
         * derive the action from the current record */
        ecs_table_record_t *trr = &table->_->records[tr->column];
        ecs_id_record_t *idrr = (ecs_id_record_t*)trr->hdr.cache;
        result = ECS_ID_ON_DELETE_OBJECT(idrr->flags);
    }
    return result;
}

static
void flecs_update_monitors_for_delete(
    ecs_world_t *world,
    ecs_id_t id)
{
    flecs_update_component_monitors(world, NULL, &(ecs_type_t){
        .array = (ecs_id_t[]){id},
        .count = 1
    });
}

static
void flecs_id_mark_for_delete(
    ecs_world_t *world,
    ecs_id_record_t *idr,
    ecs_entity_t action,
    bool delete_id)
{
    if (idr->flags & EcsIdMarkedForDelete) {
        return;
    }

    idr->flags |= EcsIdMarkedForDelete;
    flecs_marked_id_push(world, idr, action, delete_id);

    ecs_id_t id = idr->id;

    bool delete_target = flecs_id_is_delete_target(id, action);

    /* Mark all tables with the id for delete */
    ecs_table_cache_iter_t it;
    if (flecs_table_cache_iter(&idr->cache, &it)) {
        ecs_table_record_t *tr;
        while ((tr = flecs_table_cache_next(&it, ecs_table_record_t))) {
            ecs_table_t *table = tr->hdr.table;
            if (table->flags & EcsTableMarkedForDelete) {
                continue;
            }

            ecs_entity_t cur_action = flecs_get_delete_action(table, tr, action,
                delete_target);

            /* If this is a Delete action, recursively mark ids & tables */
            if (cur_action == EcsDelete) {
                table->flags |= EcsTableMarkedForDelete;
                ecs_log_push_2();
                flecs_targets_mark_for_delete(world, table);
                ecs_log_pop_2();
            } else if (cur_action == EcsPanic) {
                flecs_throw_invalid_delete(world, id);
            }
        }
    }

    /* Same for empty tables */
    if (flecs_table_cache_empty_iter(&idr->cache, &it)) {
        ecs_table_record_t *tr;
        while ((tr = flecs_table_cache_next(&it, ecs_table_record_t))) {
            tr->hdr.table->flags |= EcsTableMarkedForDelete;
        }
    }

    /* Signal query cache monitors */
    flecs_update_monitors_for_delete(world, id);

    /* If id is a wildcard pair, update cache monitors for non-wildcard ids */
    if (ecs_id_is_wildcard(id)) {
        ecs_assert(ECS_HAS_ID_FLAG(id, PAIR), ECS_INTERNAL_ERROR, NULL);
        ecs_id_record_t *cur = idr;
        if (ECS_PAIR_SECOND(id) == EcsWildcard) {
            while ((cur = cur->first.next)) {
                flecs_update_monitors_for_delete(world, cur->id);
            }
        } else {
            ecs_assert(ECS_PAIR_FIRST(id) == EcsWildcard, 
                ECS_INTERNAL_ERROR, NULL);
            while ((cur = cur->second.next)) {
                flecs_update_monitors_for_delete(world, cur->id);
            }
        }
    }
}

static
bool flecs_on_delete_mark(
    ecs_world_t *world,
    ecs_id_t id,
    ecs_entity_t action,
    bool delete_id)
{
    ecs_id_record_t *idr = flecs_id_record_get(world, id);
    if (!idr) {
        /* If there's no id record, there's nothing to delete */
        return false;
    }

    if (!action) {
        /* If no explicit action is provided, derive it */
        if (!ecs_id_is_pair(id) || ECS_PAIR_SECOND(id) == EcsWildcard) {
            /* Delete actions are determined by the component, or in the case
             * of a pair by the relationship. */
            action = ECS_ID_ON_DELETE(idr->flags);
        }
    }

    if (action == EcsPanic) {
        /* This id is protected from deletion */
        flecs_throw_invalid_delete(world, id);
        return false;
    }

    flecs_id_mark_for_delete(world, idr, action, delete_id);

    return true;
}

static
void flecs_remove_from_table(
    ecs_world_t *world, 
    ecs_table_t *table) 
{
    ecs_table_diff_t temp_diff;
    ecs_table_diff_builder_t diff = ECS_TABLE_DIFF_INIT;
    flecs_table_diff_builder_init(world, &diff);
    ecs_table_t *dst_table = table; 

    /* To find the dst table, remove all ids that are marked for deletion */
    int32_t i, t, count = ecs_vec_count(&world->store.marked_ids);
    ecs_marked_id_t *ids = ecs_vec_first(&world->store.marked_ids);
    const ecs_table_record_t *tr;
    for (i = 0; i < count; i ++) {
        const ecs_id_record_t *idr = ids[i].idr;

        if (!(tr = flecs_id_record_get_table(idr, dst_table))) {
            continue;
        }

        t = tr->column;

        do {
            ecs_id_t id = dst_table->type.array[t];
            dst_table = flecs_table_traverse_remove(
                world, dst_table, &id, &temp_diff);
            flecs_table_diff_build_append_table(world, &diff, &temp_diff);
        } while (dst_table->type.count && (t = ecs_search_offset(
            world, dst_table, t, idr->id, NULL)) != -1);
    }

    ecs_assert(dst_table != NULL, ECS_INTERNAL_ERROR, NULL);

    if (!dst_table->type.count) {
        /* If this removes all components, clear table */
        flecs_table_clear_entities(world, table);
    } else {
        /* Otherwise, merge table into dst_table */
        if (dst_table != table) {
            int32_t table_count = ecs_table_count(table);
            if (diff.removed.count && table_count) {
                ecs_log_push_3();
                ecs_table_diff_t td;
                flecs_table_diff_build_noalloc(&diff, &td);
                flecs_notify_on_remove(world, table, NULL, 0, table_count, 
                    &td.removed);
                ecs_log_pop_3();
            }

            flecs_table_merge(world, dst_table, table, 
                &dst_table->data, &table->data);
        }
    }

    flecs_table_diff_builder_fini(world, &diff);
}

static
bool flecs_on_delete_clear_tables(
    ecs_world_t *world)
{
    /* Iterate in reverse order so that DAGs get deleted bottom to top */
    int32_t i, last = ecs_vec_count(&world->store.marked_ids), first = 0;
    ecs_marked_id_t *ids = ecs_vec_first(&world->store.marked_ids);
    do {
        for (i = last - 1; i >= first; i --) {
            ecs_id_record_t *idr = ids[i].idr;
            ecs_entity_t action = ids[i].action;
 
            /* Empty all tables for id */
            ecs_table_cache_iter_t it;
            if (flecs_table_cache_iter(&idr->cache, &it)) {
                ecs_table_record_t *tr;
                while ((tr = flecs_table_cache_next(&it, ecs_table_record_t))) {
                    ecs_table_t *table = tr->hdr.table;

                    if ((action == EcsRemove) || 
                        !(table->flags & EcsTableMarkedForDelete))
                    {
                        flecs_remove_from_table(world, table);
                    } else {
                        ecs_dbg_3(
                            "#[red]delete#[reset] entities from table %u", 
                            (uint32_t)table->id);
                        flecs_table_delete_entities(world, table);
                    }
                }
            }

            /* Run commands so children get notified before parent is deleted */
            if (world->stages[0].defer) {
                flecs_defer_end(world, &world->stages[0]);
                flecs_defer_begin(world, &world->stages[0]);
            }

            /* User code (from triggers) could have enqueued more ids to delete,
             * reobtain the array in case it got reallocated */
            ids = ecs_vec_first(&world->store.marked_ids);
        }

        /* Check if new ids were marked since we started */
        int32_t new_last = ecs_vec_count(&world->store.marked_ids);
        if (new_last != last) {
            /* Iterate remaining ids */
            ecs_assert(new_last > last, ECS_INTERNAL_ERROR, NULL);
            first = last;
            last = new_last;
        } else {
            break;
        }
    } while (true);

    return true;
}

static
bool flecs_on_delete_clear_ids(
    ecs_world_t *world)
{
    int32_t i, count = ecs_vec_count(&world->store.marked_ids);
    ecs_marked_id_t *ids = ecs_vec_first(&world->store.marked_ids);
    int twice = 2;

    do {
        for (i = 0; i < count; i ++) {
            /* Release normal ids before wildcard ids */
            if (ecs_id_is_wildcard(ids[i].id)) {
                if (twice == 2) {
                    continue;
                }
            } else {
                if (twice == 1) {
                    continue;
                }
            }

            ecs_id_record_t *idr = ids[i].idr;
            bool delete_id = ids[i].delete_id;

            flecs_id_record_release_tables(world, idr);

            /* Release the claim taken by flecs_marked_id_push. This may delete the
             * id record as all other claims may have been released. */
            int32_t rc = flecs_id_record_release(world, idr);
            ecs_assert(rc >= 0, ECS_INTERNAL_ERROR, NULL);
            (void)rc;

            /* If rc is 0, the id was likely deleted by a nested delete_with call
             * made by an on_remove handler/OnRemove observer */
            if (rc) {
                if (delete_id) {
                    /* If id should be deleted, release initial claim. This happens when
                     * a component, tag, or part of a pair is deleted. */
                    flecs_id_record_release(world, idr);
                } else {
                    /* If id should not be deleted, unmark id record for deletion. This
                     * happens when all instances *of* an id are deleted, for example
                     * when calling ecs_remove_all or ecs_delete_with. */
                    idr->flags &= ~EcsIdMarkedForDelete;
                }
            }
        }
    } while (-- twice);

    return true;
}

static
void flecs_on_delete(
    ecs_world_t *world,
    ecs_id_t id,
    ecs_entity_t action,
    bool delete_id)
{
    /* Cleanup can happen recursively. If a cleanup action is already in 
     * progress, only append ids to the marked_ids. The topmost cleanup
     * frame will handle the actual cleanup. */
    int32_t count = ecs_vec_count(&world->store.marked_ids);

    /* Make sure we're evaluating a consistent list of non-empty tables */
    ecs_run_aperiodic(world, EcsAperiodicEmptyTables);

    /* Collect all ids that need to be deleted */
    flecs_on_delete_mark(world, id, action, delete_id);

    /* Only perform cleanup if we're the first stack frame doing it */
    if (!count && ecs_vec_count(&world->store.marked_ids)) {
        ecs_dbg_2("#[red]delete#[reset]");
        ecs_log_push_2();

        /* Empty tables with all the to be deleted ids */
        flecs_on_delete_clear_tables(world);

        /* All marked tables are empty, ensure they're in the right list */
        ecs_run_aperiodic(world, EcsAperiodicEmptyTables);

        /* Release remaining references to the ids */
        flecs_on_delete_clear_ids(world);

        /* Verify deleted ids are no longer in use */
#ifdef FLECS_DEBUG
        ecs_marked_id_t *ids = ecs_vec_first(&world->store.marked_ids);
        int32_t i; count = ecs_vec_count(&world->store.marked_ids);
        for (i = 0; i < count; i ++) {
            ecs_assert(!ecs_id_in_use(world, ids[i].id), 
                ECS_INTERNAL_ERROR, NULL);
        }
#endif
        ecs_assert(!ecs_id_in_use(world, id), ECS_INTERNAL_ERROR, NULL);

        /* Ids are deleted, clear stack */
        ecs_vec_clear(&world->store.marked_ids);

        ecs_log_pop_2();
    }
}

void ecs_delete_with(
    ecs_world_t *world,
    ecs_id_t id)
{
    flecs_journal_begin(world, EcsJournalDeleteWith, id, NULL, NULL);

    ecs_stage_t *stage = flecs_stage_from_world(&world);
    if (flecs_defer_on_delete_action(stage, id, EcsDelete)) {
        return;
    }

    flecs_on_delete(world, id, EcsDelete, false);
    flecs_defer_end(world, stage);

    flecs_journal_end();
}

void ecs_remove_all(
    ecs_world_t *world,
    ecs_id_t id)
{
    flecs_journal_begin(world, EcsJournalRemoveAll, id, NULL, NULL);

    ecs_stage_t *stage = flecs_stage_from_world(&world);
    if (flecs_defer_on_delete_action(stage, id, EcsRemove)) {
        return;
    }

    flecs_on_delete(world, id, EcsRemove, false);
    flecs_defer_end(world, stage);

    flecs_journal_end();
}

void ecs_delete(
    ecs_world_t *world,
    ecs_entity_t entity)
{
    ecs_check(world != NULL, ECS_INVALID_PARAMETER, NULL);
    ecs_check(entity != 0, ECS_INVALID_PARAMETER, NULL);

    ecs_stage_t *stage = flecs_stage_from_world(&world);
    if (flecs_defer_delete(stage, entity)) {
        return;
    }

    ecs_record_t *r = flecs_entities_try(world, entity);
    if (r) {
        flecs_journal_begin(world, EcsJournalDelete, entity, NULL, NULL);

        ecs_flags32_t row_flags = ECS_RECORD_TO_ROW_FLAGS(r->row);
        ecs_table_t *table;
        if (row_flags) {
            if (row_flags & EcsEntityIsTarget) {
                flecs_on_delete(world, ecs_pair(EcsFlag, entity), 0, true);
                flecs_on_delete(world, ecs_pair(EcsWildcard, entity), 0, true);
                r->idr = NULL;
            }
            if (row_flags & EcsEntityIsId) {
                flecs_on_delete(world, entity, 0, true);
                flecs_on_delete(world, ecs_pair(entity, EcsWildcard), 0, true);
            }
            if (row_flags & EcsEntityIsTraversable) {
                table = r->table;
                if (table) {
                    flecs_table_traversable_add(table, -1);
                }
            }
            /* Merge operations before deleting entity */
            flecs_defer_end(world, stage);
            flecs_defer_begin(world, stage);
        }

        table = r->table;

        if (table) {
            ecs_table_diff_t diff = {
                .removed = table->type
            };

            flecs_delete_entity(world, r, &diff);

            r->row = 0;
            r->table = NULL;
        }
        
        flecs_entities_remove(world, entity);

        flecs_journal_end();
    }

    flecs_defer_end(world, stage);
error:
    return;
}

void ecs_add_id(
    ecs_world_t *world,
    ecs_entity_t entity,
    ecs_id_t id)
{
    ecs_check(world != NULL, ECS_INVALID_PARAMETER, NULL);
    ecs_check(ecs_is_alive(world, entity), ECS_INVALID_PARAMETER, NULL);
    ecs_check(ecs_id_is_valid(world, id), ECS_INVALID_PARAMETER, NULL);
    flecs_add_id(world, entity, id);
error:
    return;
}

void ecs_remove_id(
    ecs_world_t *world,
    ecs_entity_t entity,
    ecs_id_t id)
{
    ecs_check(world != NULL, ECS_INVALID_PARAMETER, NULL);
    ecs_check(ecs_is_alive(world, entity), ECS_INVALID_PARAMETER, NULL);
    ecs_check(ecs_id_is_valid(world, id) || ecs_id_is_wildcard(id), 
        ECS_INVALID_PARAMETER, NULL);
    flecs_remove_id(world, entity, id);
error:
    return;
}

void ecs_override_id(
    ecs_world_t *world,
    ecs_entity_t entity,
    ecs_id_t id)
{
    ecs_check(ecs_id_is_valid(world, id), ECS_INVALID_PARAMETER, NULL);
    ecs_add_id(world, entity, ECS_OVERRIDE | id);
error:
    return;
}

ecs_entity_t ecs_clone(
    ecs_world_t *world,
    ecs_entity_t dst,
    ecs_entity_t src,
    bool copy_value)
{
    ecs_check(world != NULL, ECS_INVALID_PARAMETER, NULL);
    ecs_check(src != 0, ECS_INVALID_PARAMETER, NULL);
    ecs_check(ecs_is_alive(world, src), ECS_INVALID_PARAMETER, NULL);
    ecs_check(!dst || !ecs_get_table(world, dst), ECS_INVALID_PARAMETER, NULL);

    ecs_stage_t *stage = flecs_stage_from_world(&world);
    if (!dst) {
        dst = ecs_new_id(world);
    }

    if (flecs_defer_clone(stage, dst, src, copy_value)) {
        return dst;
    }

    ecs_record_t *src_r = flecs_entities_get(world, src);
    ecs_assert(src_r != NULL, ECS_INTERNAL_ERROR, NULL);
    ecs_table_t *src_table = src_r->table;
    if (!src_table) {
        goto done;
    }

    ecs_type_t src_type = src_table->type;
    ecs_table_diff_t diff = { .added = src_type };
    ecs_record_t *dst_r = flecs_entities_get(world, dst);
    flecs_new_entity(world, dst, dst_r, src_table, &diff, true, true);
    int32_t row = ECS_RECORD_TO_ROW(dst_r->row);

    if (copy_value) {
        flecs_table_move(world, dst, src, src_table,
            row, src_table, ECS_RECORD_TO_ROW(src_r->row), true);
        flecs_notify_on_set(world, src_table, row, 1, NULL, true);
    }

done:
    flecs_defer_end(world, stage);
    return dst;
error:
    return 0;
}

const void* ecs_get_id(
    const ecs_world_t *world,
    ecs_entity_t entity,
    ecs_id_t id)
{
    ecs_check(world != NULL, ECS_INVALID_PARAMETER, NULL);
    ecs_check(ecs_is_alive(world, entity), ECS_INVALID_PARAMETER, NULL);
    ecs_check(flecs_stage_from_readonly_world(world)->async == false, 
        ECS_INVALID_PARAMETER, NULL);

    world = ecs_get_world(world);

    ecs_record_t *r = flecs_entities_get(world, entity);
    ecs_assert(r != NULL, ECS_INVALID_PARAMETER, NULL);

    ecs_table_t *table = r->table;
    if (!table) {
        return NULL;
    }

    ecs_id_record_t *idr = flecs_id_record_get(world, id);
    if (!idr) {
        return NULL;
    }

    const ecs_table_record_t *tr = NULL;
    ecs_table_t *storage_table = table->storage_table;
    if (storage_table) {
        tr = flecs_id_record_get_table(idr, storage_table);
    } else {
        /* If the entity does not have a storage table (has no data) but it does
         * have the id, the id must be a tag, and getting a tag is illegal. */
        ecs_check(!ecs_owns_id(world, entity, id), ECS_NOT_A_COMPONENT, NULL);
    }

    if (!tr) {
       return flecs_get_base_component(world, table, id, idr, 0);
    }

    int32_t row = ECS_RECORD_TO_ROW(r->row);
    return flecs_get_component_w_index(table, tr->column, row).ptr;
error:
    return NULL;
}

void* ecs_get_mut_id(
    ecs_world_t *world,
    ecs_entity_t entity,
    ecs_id_t id)
{
    ecs_check(world != NULL, ECS_INVALID_PARAMETER, NULL);
    ecs_check(ecs_is_alive(world, entity), ECS_INVALID_PARAMETER, NULL);
    ecs_check(ecs_id_is_valid(world, id), ECS_INVALID_PARAMETER, NULL);

    ecs_stage_t *stage = flecs_stage_from_world(&world);
    if (flecs_defer_cmd(stage)) {
        return flecs_defer_set(
            world, stage, EcsOpMut, entity, id, 0, NULL, true);
    }

    ecs_record_t *r = flecs_entities_get(world, entity);
    ecs_assert(r != NULL, ECS_INTERNAL_ERROR, NULL);
    void *result = flecs_get_mut(world, entity, id, r).ptr;
    ecs_check(result != NULL, ECS_INVALID_PARAMETER, NULL);

    flecs_defer_end(world, stage);
    return result;
error:
    return NULL;
}

static
ecs_record_t* flecs_access_begin(
    ecs_world_t *stage,
    ecs_entity_t entity,
    bool write)
{
    ecs_check(ecs_os_has_threading(), ECS_MISSING_OS_API, NULL);

    const ecs_world_t *world = ecs_get_world(stage);
    ecs_record_t *r = flecs_entities_get(world, entity);
    ecs_assert(r != NULL, ECS_INTERNAL_ERROR, NULL);

    ecs_table_t *table;
    if (!(table = r->table)) {
        return NULL;
    }

    int32_t count = ecs_os_ainc(&table->_->lock);
    (void)count;
    if (write) {
        ecs_check(count == 1, ECS_ACCESS_VIOLATION, NULL);
    }

    return r;
error:
    return NULL;
}

static
void flecs_access_end(
    const ecs_record_t *r,
    bool write)
{
    ecs_check(ecs_os_has_threading(), ECS_MISSING_OS_API, NULL);
    ecs_check(r != NULL, ECS_INVALID_PARAMETER, NULL);
    ecs_check(r->table != NULL, ECS_INVALID_PARAMETER, NULL);
    int32_t count = ecs_os_adec(&r->table->_->lock);
    (void)count;
    if (write) {
        ecs_check(count == 0, ECS_ACCESS_VIOLATION, NULL);
    }
    ecs_check(count >= 0, ECS_ACCESS_VIOLATION, NULL);

error:
    return;
}

ecs_record_t* ecs_write_begin(
    ecs_world_t *world,
    ecs_entity_t entity)
{
    return flecs_access_begin(world, entity, true);
}

void ecs_write_end(
    ecs_record_t *r)
{
    flecs_access_end(r, true);
}

const ecs_record_t* ecs_read_begin(
    ecs_world_t *world,
    ecs_entity_t entity)
{
    return flecs_access_begin(world, entity, false);
}

void ecs_read_end(
    const ecs_record_t *r)
{
    flecs_access_end(r, false);
}

ecs_entity_t ecs_record_get_entity(
    const ecs_record_t *record)
{
    ecs_check(record != NULL, ECS_INVALID_PARAMETER, NULL);
    ecs_table_t *table = record->table;
    if (!table) {
        return 0;
    }

    return ecs_vec_get_t(&table->data.entities, ecs_entity_t, 
        ECS_RECORD_TO_ROW(record->row))[0];
error:
    return 0;
}

const void* ecs_record_get_id(
    ecs_world_t *stage,
    const ecs_record_t *r,
    ecs_id_t id)
{
    const ecs_world_t *world = ecs_get_world(stage);
    return flecs_get_component(world, r->table, ECS_RECORD_TO_ROW(r->row), id);
}

bool ecs_record_has_id(
    ecs_world_t *stage,
    const ecs_record_t *r,
    ecs_id_t id)
{
    const ecs_world_t *world = ecs_get_world(stage);
    if (r->table) {
        return ecs_table_has_id(world, r->table, id);
    }
    return false;
}

void* ecs_record_get_mut_id(
    ecs_world_t *stage,
    ecs_record_t *r,
    ecs_id_t id)
{
    const ecs_world_t *world = ecs_get_world(stage);
    return flecs_get_component(world, r->table, ECS_RECORD_TO_ROW(r->row), id);
}

ecs_ref_t ecs_ref_init_id(
    const ecs_world_t *world,
    ecs_entity_t entity,
    ecs_id_t id)
{
    ecs_check(ecs_is_alive(world, entity), ECS_INVALID_PARAMETER, NULL);
    ecs_check(ecs_id_is_valid(world, id), ECS_INVALID_PARAMETER, NULL);
    
    world = ecs_get_world(world);

    ecs_record_t *record = flecs_entities_get(world, entity);
    ecs_check(record != NULL, ECS_INVALID_PARAMETER,
        "cannot create ref for empty entity");

    ecs_ref_t result = {
        .entity = entity,
        .id = id,
        .record = record
    };

    ecs_table_t *table = record->table;
    if (table) {
        result.tr = flecs_table_record_get(world, table, id);
    }

    return result;
error:
    return (ecs_ref_t){0};
}

void ecs_ref_update(
    const ecs_world_t *world,
    ecs_ref_t *ref)
{
    ecs_check(world != NULL, ECS_INVALID_PARAMETER, NULL);
    ecs_check(ref != NULL, ECS_INVALID_PARAMETER, NULL);
    ecs_check(ref->entity != 0, ECS_INVALID_PARAMETER, NULL);
    ecs_check(ref->id != 0, ECS_INVALID_PARAMETER, NULL);
    ecs_check(ref->record != NULL, ECS_INVALID_PARAMETER, NULL);

    ecs_record_t *r = ref->record;
    ecs_table_t *table = r->table;
    if (!table) {
        return;
    }

    ecs_table_record_t *tr = ref->tr;
    if (!tr || tr->hdr.table != table) {
        tr = ref->tr = flecs_table_record_get(world, table, ref->id);
        if (!tr) {
            return;
        }

        ecs_assert(tr->hdr.table == r->table, ECS_INTERNAL_ERROR, NULL);
    }
error:
    return;
}

void* ecs_ref_get_id(
    const ecs_world_t *world,
    ecs_ref_t *ref,
    ecs_id_t id)
{
    ecs_check(world != NULL, ECS_INVALID_PARAMETER, NULL);
    ecs_check(ref != NULL, ECS_INVALID_PARAMETER, NULL);
    ecs_check(ref->entity != 0, ECS_INVALID_PARAMETER, NULL);
    ecs_check(ref->id != 0, ECS_INVALID_PARAMETER, NULL);
    ecs_check(ref->record != NULL, ECS_INVALID_PARAMETER, NULL);
    ecs_check(id == ref->id, ECS_INVALID_PARAMETER, NULL);

    ecs_record_t *r = ref->record;
    ecs_table_t *table = r->table;
    if (!table) {
        return NULL;
    }

    int32_t row = ECS_RECORD_TO_ROW(r->row);
    ecs_check(row < ecs_table_count(table), ECS_INTERNAL_ERROR, NULL);

    ecs_table_record_t *tr = ref->tr;
    if (!tr || tr->hdr.table != table) {
        tr = ref->tr = flecs_table_record_get(world, table, id);
        if (!tr) {
            return NULL;
        }

        ecs_assert(tr->hdr.table == r->table, ECS_INTERNAL_ERROR, NULL);
    }

    int32_t column = ecs_table_type_to_storage_index(table, tr->column);
    ecs_assert(column != -1, ECS_INTERNAL_ERROR, NULL);
    return flecs_get_component_w_index(table, column, row).ptr;
error:
    return NULL;
}

void* ecs_emplace_id(
    ecs_world_t *world,
    ecs_entity_t entity,
    ecs_id_t id)
{
    ecs_check(world != NULL, ECS_INVALID_PARAMETER, NULL);
    ecs_check(ecs_is_alive(world, entity), ECS_INVALID_PARAMETER, NULL);
    ecs_check(ecs_id_is_valid(world, id), ECS_INVALID_PARAMETER, NULL);
    ecs_check(!ecs_has_id(world, entity, id), ECS_INVALID_PARAMETER, 
        "cannot emplace a component the entity already has");

    ecs_stage_t *stage = flecs_stage_from_world(&world);

    if (flecs_defer_cmd(stage)) {
        return flecs_defer_set(world, stage, EcsOpEmplace, entity, id, 0, NULL, 
            true);
    }

    ecs_record_t *r = flecs_entities_get(world, entity);
    flecs_add_id_w_record(world, entity, r, id, false /* Add without ctor */);
    flecs_defer_end(world, stage);

    void *ptr = flecs_get_component(world, r->table, ECS_RECORD_TO_ROW(r->row), id);
    ecs_check(ptr != NULL, ECS_INVALID_PARAMETER, NULL);

    return ptr;
error:
    return NULL;
}

static
void flecs_modified_id_if(
    ecs_world_t *world,
    ecs_entity_t entity,
    ecs_id_t id)
{
    ecs_check(world != NULL, ECS_INVALID_PARAMETER, NULL);
    ecs_check(ecs_is_alive(world, entity), ECS_INVALID_PARAMETER, NULL);
    ecs_check(ecs_id_is_valid(world, id), ECS_INVALID_PARAMETER, NULL);

    ecs_stage_t *stage = flecs_stage_from_world(&world);

    if (flecs_defer_modified(stage, entity, id)) {
        return;
    }

    ecs_record_t *r = flecs_entities_get(world, entity);
    ecs_table_t *table = r->table;
    if (!flecs_table_record_get(world, table, id)) {
        flecs_defer_end(world, stage);
        return;
    }

    ecs_type_t ids = { .array = &id, .count = 1 };
    flecs_notify_on_set(world, table, ECS_RECORD_TO_ROW(r->row), 1, &ids, true);

    flecs_table_mark_dirty(world, table, id);
    flecs_defer_end(world, stage);
error:
    return;
}

void ecs_modified_id(
    ecs_world_t *world,
    ecs_entity_t entity,
    ecs_id_t id)
{
    ecs_check(world != NULL, ECS_INVALID_PARAMETER, NULL);
    ecs_check(ecs_is_alive(world, entity), ECS_INVALID_PARAMETER, NULL);
    ecs_check(ecs_id_is_valid(world, id), ECS_INVALID_PARAMETER, NULL);

    ecs_stage_t *stage = flecs_stage_from_world(&world);

    if (flecs_defer_modified(stage, entity, id)) {
        return;
    }

    /* If the entity does not have the component, calling ecs_modified is 
     * invalid. The assert needs to happen after the defer statement, as the
     * entity may not have the component when this function is called while
     * operations are being deferred. */
    ecs_check(ecs_has_id(world, entity, id), ECS_INVALID_PARAMETER, NULL);

    ecs_record_t *r = flecs_entities_get(world, entity);
    ecs_table_t *table = r->table;
    ecs_type_t ids = { .array = &id, .count = 1 };
    flecs_notify_on_set(world, table, ECS_RECORD_TO_ROW(r->row), 1, &ids, true);

    flecs_table_mark_dirty(world, table, id);
    flecs_defer_end(world, stage);
error:
    return;
}

static
void flecs_copy_ptr_w_id(
    ecs_world_t *world,
    ecs_stage_t *stage,
    ecs_entity_t entity,
    ecs_id_t id,
    size_t size,
    void *ptr)
{
    if (flecs_defer_cmd(stage)) {
        flecs_defer_set(world, stage, EcsOpSet, entity, id, 
            flecs_utosize(size), ptr, false);
        return;
    }

    ecs_record_t *r = flecs_entities_get(world, entity);
    flecs_component_ptr_t dst = flecs_get_mut(world, entity, id, r);
    const ecs_type_info_t *ti = dst.ti;
    ecs_check(dst.ptr != NULL, ECS_INVALID_PARAMETER, NULL);

    if (ptr) {
        ecs_copy_t copy = ti->hooks.copy;
        if (copy) {
            copy(dst.ptr, ptr, 1, ti);
        } else {
            ecs_os_memcpy(dst.ptr, ptr, flecs_utosize(size));
        }
    } else {
        ecs_os_memset(dst.ptr, 0, size);
    }

    flecs_table_mark_dirty(world, r->table, id);

    ecs_table_t *table = r->table;
    if (table->flags & EcsTableHasOnSet || ti->hooks.on_set) {
        ecs_type_t ids = { .array = &id, .count = 1 };
        flecs_notify_on_set(
            world, table, ECS_RECORD_TO_ROW(r->row), 1, &ids, true);
    }

    flecs_defer_end(world, stage);
error:
    return;
}

static
void flecs_move_ptr_w_id(
    ecs_world_t *world,
    ecs_stage_t *stage,
    ecs_entity_t entity,
    ecs_id_t id,
    size_t size,
    void *ptr,
    ecs_cmd_kind_t cmd_kind)
{
    if (flecs_defer_cmd(stage)) {
        flecs_defer_set(world, stage, cmd_kind, entity, id, 
            flecs_utosize(size), ptr, false);
        return;
    }

    ecs_record_t *r = flecs_entities_get(world, entity);
    flecs_component_ptr_t dst = flecs_get_mut(world, entity, id, r);
    ecs_check(dst.ptr != NULL, ECS_INVALID_PARAMETER, NULL);

    const ecs_type_info_t *ti = dst.ti;
    ecs_assert(ti != NULL, ECS_INTERNAL_ERROR, NULL);
    ecs_move_t move;
    if (cmd_kind != EcsOpEmplace) {
        /* ctor will have happened by get_mut */
        move = ti->hooks.move_dtor;
    } else {
        move = ti->hooks.ctor_move_dtor;
    }
    if (move) {
        move(dst.ptr, ptr, 1, ti);
    } else {
        ecs_os_memcpy(dst.ptr, ptr, flecs_utosize(size));
    }

    flecs_table_mark_dirty(world, r->table, id);

    if (cmd_kind == EcsOpSet) {
        ecs_table_t *table = r->table;
        if (table->flags & EcsTableHasOnSet || ti->hooks.on_set) {
            ecs_type_t ids = { .array = &id, .count = 1 };
            flecs_notify_on_set(
                world, table, ECS_RECORD_TO_ROW(r->row), 1, &ids, true);
        }
    }

    flecs_defer_end(world, stage);
error:
    return;
}

ecs_entity_t ecs_set_id(
    ecs_world_t *world,
    ecs_entity_t entity,
    ecs_id_t id,
    size_t size,
    const void *ptr)
{
    ecs_check(world != NULL, ECS_INVALID_PARAMETER, NULL);
    ecs_check(!entity || ecs_is_alive(world, entity), ECS_INVALID_PARAMETER, NULL);
    ecs_check(ecs_id_is_valid(world, id), ECS_INVALID_PARAMETER, NULL);

    ecs_stage_t *stage = flecs_stage_from_world(&world);

    if (!entity) {
        entity = ecs_new_id(world);
        ecs_entity_t scope = stage->scope;
        if (scope) {
            ecs_add_pair(world, entity, EcsChildOf, scope);
        }
    }

    /* Safe to cast away const: function won't modify if move arg is false */
    flecs_copy_ptr_w_id(world, stage, entity, id, size, (void*)ptr);
    return entity;
error:
    return 0;
}

void ecs_enable_id(
    ecs_world_t *world,
    ecs_entity_t entity,
    ecs_id_t id,
    bool enable)
{
    ecs_check(world != NULL, ECS_INVALID_PARAMETER, NULL);
    ecs_check(ecs_is_valid(world, entity), ECS_INVALID_PARAMETER, NULL);
    ecs_check(ecs_id_is_valid(world, id), ECS_INVALID_PARAMETER, NULL);

    ecs_stage_t *stage = flecs_stage_from_world(&world);

    if (flecs_defer_enable(stage, entity, id, enable)) {
        return;
    } else {
        /* Operations invoked by enable/disable should not be deferred */
        stage->defer --;
    }

    ecs_record_t *r = flecs_entities_get(world, entity);
    ecs_entity_t bs_id = id | ECS_TOGGLE;
    
    ecs_table_t *table = r->table;
    int32_t index = -1;
    if (table) {
        index = ecs_search(world, table, bs_id, 0);
    }

    if (index == -1) {
        ecs_add_id(world, entity, bs_id);
        ecs_enable_id(world, entity, id, enable);
        return;
    }
    
    ecs_assert(table->_ != NULL, ECS_INTERNAL_ERROR, NULL);
    index -= table->_->bs_offset;
    ecs_assert(index >= 0, ECS_INTERNAL_ERROR, NULL);

    /* Data cannot be NULl, since entity is stored in the table */
    ecs_bitset_t *bs = &table->_->bs_columns[index];
    ecs_assert(bs != NULL, ECS_INTERNAL_ERROR, NULL);

    flecs_bitset_set(bs, ECS_RECORD_TO_ROW(r->row), enable);
error:
    return;
}

bool ecs_is_enabled_id(
    const ecs_world_t *world,
    ecs_entity_t entity,
    ecs_id_t id)
{
    ecs_check(world != NULL, ECS_INVALID_PARAMETER, NULL);
    ecs_check(ecs_is_alive(world, entity), ECS_INVALID_PARAMETER, NULL);
    ecs_check(ecs_id_is_valid(world, id), ECS_INVALID_PARAMETER, NULL);

    /* Make sure we're not working with a stage */
    world = ecs_get_world(world);

    ecs_record_t *r = flecs_entities_get(world, entity);
    ecs_assert(r != NULL, ECS_INTERNAL_ERROR, NULL);
    ecs_table_t *table = r->table;
    if (!table) {
        return false;
    }

    ecs_entity_t bs_id = id | ECS_TOGGLE;
    int32_t index = ecs_search(world, table, bs_id, 0);
    if (index == -1) {
        /* If table does not have TOGGLE column for component, component is
         * always enabled, if the entity has it */
        return ecs_has_id(world, entity, id);
    }

    ecs_assert(table->_ != NULL, ECS_INTERNAL_ERROR, NULL);
    index -= table->_->bs_offset;
    ecs_assert(index >= 0, ECS_INTERNAL_ERROR, NULL);
    ecs_bitset_t *bs = &table->_->bs_columns[index];

    return flecs_bitset_get(bs, ECS_RECORD_TO_ROW(r->row));
error:
    return false;
}

bool ecs_has_id(
    const ecs_world_t *world,
    ecs_entity_t entity,
    ecs_id_t id)
{
    ecs_check(world != NULL, ECS_INVALID_PARAMETER, NULL);
    ecs_check(ecs_is_alive(world, entity), ECS_INVALID_PARAMETER, NULL);
    ecs_check(id != 0, ECS_INVALID_PARAMETER, NULL);

    /* Make sure we're not working with a stage */
    world = ecs_get_world(world);

    ecs_record_t *r = flecs_entities_get_any(world, entity);
    ecs_assert(r != NULL, ECS_INTERNAL_ERROR, NULL);
    ecs_table_t *table = r->table;
    if (!table) {
        return false;
    }

    ecs_id_record_t *idr = flecs_id_record_get(world, id);
    int32_t column;
    if (idr) {
        const ecs_table_record_t *tr = flecs_id_record_get_table(idr, table);
        if (tr) {
            return true;
        }
    }

    if (!(table->flags & (EcsTableHasUnion|EcsTableHasIsA))) {
        return false;
    }

    ecs_table_record_t *tr;
    column = ecs_search_relation(world, table, 0, id, 
        EcsIsA, 0, 0, 0, &tr);
    if (column == -1) {
        return false;
    }

    table = tr->hdr.table;
    if ((table->flags & EcsTableHasUnion) && ECS_HAS_ID_FLAG(id, PAIR) &&
        ECS_PAIR_SECOND(id) != EcsWildcard) 
    {
        if (ECS_PAIR_FIRST(table->type.array[column]) == EcsUnion) {
            ecs_assert(table->_ != NULL, ECS_INTERNAL_ERROR, NULL);
            ecs_switch_t *sw = &table->_->sw_columns[
                column - table->_->sw_offset];
            int32_t row = ECS_RECORD_TO_ROW(r->row);
            uint64_t value = flecs_switch_get(sw, row);
            return value == ECS_PAIR_SECOND(id);
        }
    }
    
    return true;
error:
    return false;
}

bool ecs_owns_id(
    const ecs_world_t *world,
    ecs_entity_t entity,
    ecs_id_t id)
{
    return (ecs_search(world, ecs_get_table(world, entity), id, 0) != -1);
}

ecs_entity_t ecs_get_target(
    const ecs_world_t *world,
    ecs_entity_t entity,
    ecs_entity_t rel,
    int32_t index)
{
    ecs_check(world != NULL, ECS_INVALID_PARAMETER, NULL);
    ecs_check(ecs_is_alive(world, entity), ECS_INVALID_PARAMETER, NULL);
    ecs_check(rel != 0, ECS_INVALID_PARAMETER, NULL);

    world = ecs_get_world(world);

    ecs_record_t *r = flecs_entities_get(world, entity);
    ecs_assert(r != NULL, ECS_INTERNAL_ERROR, NULL);
    ecs_table_t *table = r->table;
    if (!table) {
        goto not_found;
    }

    ecs_id_t wc = ecs_pair(rel, EcsWildcard);
    ecs_id_record_t *idr = flecs_id_record_get(world, wc);
    const ecs_table_record_t *tr = NULL;
    if (idr) {
        tr = flecs_id_record_get_table(idr, table);
    }
    if (!tr) {
        if (table->flags & EcsTableHasUnion) {
            wc = ecs_pair(EcsUnion, rel);
            tr = flecs_table_record_get(world, table, wc);
            if (tr) {
                ecs_assert(table->_ != NULL, ECS_INTERNAL_ERROR, NULL);
                ecs_switch_t *sw = &table->_->sw_columns[
                    tr->column - table->_->sw_offset];
                int32_t row = ECS_RECORD_TO_ROW(r->row);
                return flecs_switch_get(sw, row);
                
            }
        }

        if (!idr || !(idr->flags & EcsIdDontInherit)) {
            goto look_in_base;
        } else {
            return 0;
        }
    } else if (table->flags & EcsTableHasTarget) {
        EcsTarget *tf = ecs_table_get_id(world, table, 
            ecs_pair_t(EcsTarget, rel), ECS_RECORD_TO_ROW(r->row));
        if (tf) {
            return ecs_record_get_entity(tf->target);
        }
    }

    if (index >= tr->count) {
        index -= tr->count;
        goto look_in_base;
    }

    return ecs_pair_second(world, table->type.array[tr->column + index]);
look_in_base:
    if (table->flags & EcsTableHasIsA) {
        const ecs_table_record_t *isa_tr = flecs_id_record_get_table(
            world->idr_isa_wildcard, table);
        ecs_assert(isa_tr != NULL, ECS_INTERNAL_ERROR, NULL);

        ecs_id_t *ids = table->type.array;
        int32_t i = isa_tr->column, end = (i + isa_tr->count);
        for (; i < end; i ++) {
            ecs_id_t isa_pair = ids[i];
            ecs_entity_t base = ecs_pair_second(world, isa_pair);
            ecs_assert(base != 0, ECS_INTERNAL_ERROR, NULL);
            ecs_entity_t t = ecs_get_target(world, base, rel, index);
            if (t) {
                return t;
            }
        }
    }

not_found:
error:
    return 0;
}

ecs_entity_t ecs_get_parent(
    const ecs_world_t *world,
    ecs_entity_t entity)
{
    return ecs_get_target(world, entity, EcsChildOf, 0);
}

ecs_entity_t ecs_get_target_for_id(
    const ecs_world_t *world,
    ecs_entity_t entity,
    ecs_entity_t rel,
    ecs_id_t id)
{
    ecs_check(world != NULL, ECS_INVALID_PARAMETER, NULL);
    ecs_check(ecs_is_alive(world, entity), ECS_INVALID_PARAMETER, NULL);

    if (!id) {
        return ecs_get_target(world, entity, rel, 0);
    }

    world = ecs_get_world(world);

    ecs_table_t *table = ecs_get_table(world, entity);
    ecs_entity_t subject = 0;

    if (rel) {
        int32_t column = ecs_search_relation(
            world, table, 0, id, rel, 0, &subject, 0, 0);
        if (column == -1) {
            return 0;
        }
    } else {
        entity = 0; /* Don't return entity if id was not found */

        if (table) {
            ecs_id_t *ids = table->type.array;
            int32_t i, count = table->type.count;

            for (i = 0; i < count; i ++) {
                ecs_id_t ent = ids[i];
                if (ent & ECS_ID_FLAGS_MASK) {
                    /* Skip ids with pairs, roles since 0 was provided for rel */
                    break;
                }

                if (ecs_has_id(world, ent, id)) {
                    subject = ent;
                    break;
                }
            }
        }
    }

    if (subject == 0) {
        return entity;
    } else {
        return subject;
    }
error:
    return 0;
}

int32_t ecs_get_depth(
    const ecs_world_t *world,
    ecs_entity_t entity,
    ecs_entity_t rel)
{
    ecs_check(ecs_is_valid(world, rel), ECS_INVALID_PARAMETER, NULL);
    ecs_check(ecs_has_id(world, rel, EcsAcyclic), ECS_INVALID_PARAMETER, NULL);

    ecs_table_t *table = ecs_get_table(world, entity);
    if (table) {
        return ecs_table_get_depth(world, table, rel);
    }

    return 0;
error:
    return -1;
}

static
ecs_entity_t flecs_id_for_depth(
    ecs_world_t *world,
    int32_t depth)
{
    ecs_vec_t *depth_ids = &world->store.depth_ids;
    int32_t i, count = ecs_vec_count(depth_ids);
    for (i = count; i <= depth; i ++) {
        ecs_entity_t *el = ecs_vec_append_t(
            &world->allocator, depth_ids, ecs_entity_t);
        el[0] = ecs_new_w_pair(world, EcsChildOf, EcsFlecsInternals);
        ecs_map_val_t *v = ecs_map_ensure(&world->store.entity_to_depth, el[0]);
        v[0] = flecs_ito(uint64_t, i);
    }

    return ecs_vec_get_t(&world->store.depth_ids, ecs_entity_t, depth)[0];
}

static
int32_t flecs_depth_for_id(
    ecs_world_t *world,
    ecs_entity_t id)
{
    ecs_map_val_t *v = ecs_map_get(&world->store.entity_to_depth, id);
    ecs_assert(v != NULL, ECS_INTERNAL_ERROR, NULL);
    return flecs_uto(int32_t, v[0]);
}

static
int32_t flecs_depth_for_flat_table(
    ecs_world_t *world,
    ecs_table_t *table)
{
    ecs_assert(table->flags & EcsTableHasTarget, ECS_INTERNAL_ERROR, NULL);
    ecs_id_t id;
    int32_t col = ecs_search(world, table, 
        ecs_pair(EcsFlatten, EcsWildcard), &id);
    ecs_assert(col != -1, ECS_INTERNAL_ERROR, NULL);
    (void)col;
    return flecs_depth_for_id(world, ECS_PAIR_SECOND(id));
}

static
void flecs_flatten(
    ecs_world_t *world,
    ecs_entity_t root,
    ecs_id_t pair,
    int32_t depth,
    const ecs_flatten_desc_t *desc)
{
    ecs_id_record_t *idr = flecs_id_record_get(world, pair);
    if (!idr) {
        return;
    }

    ecs_entity_t depth_id = flecs_id_for_depth(world, depth);
    ecs_id_t root_pair = ecs_pair(EcsChildOf, root);
    ecs_id_t tgt_pair = ecs_pair_t(EcsTarget, EcsChildOf);
    ecs_id_t depth_pair = ecs_pair(EcsFlatten, depth_id);
    ecs_id_t name_pair = ecs_pair_t(EcsIdentifier, EcsName);

    ecs_entity_t rel = ECS_PAIR_FIRST(pair);
    ecs_table_cache_iter_t it;
    if (idr && flecs_table_cache_iter((ecs_table_cache_t*)idr, &it)) {
        const ecs_table_record_t *tr;
        while ((tr = flecs_table_cache_next(&it, ecs_table_record_t))) {
            ecs_table_t *table = tr->hdr.table;
            int32_t i, count = ecs_table_count(table);
            bool has_tgt = table->flags & EcsTableHasTarget;
            flecs_emit_propagate_invalidate(world, table, 0, count);

            ecs_record_t **records = table->data.records.array;
            ecs_entity_t *entities = table->data.entities.array;
            for (i = 0; i < count; i ++) {
                ecs_flags32_t flags = ECS_RECORD_TO_ROW_FLAGS(records[i]->row);
                if (flags & EcsEntityIsTarget) {
                    flecs_flatten(world, root, ecs_pair(rel, entities[i]), 
                        depth + 1, desc);
                }
            }

            ecs_table_diff_t tmpdiff;
            ecs_table_diff_builder_t diff = ECS_TABLE_DIFF_INIT;
            flecs_table_diff_builder_init(world, &diff);
            ecs_table_t *dst;

            dst = flecs_table_traverse_add(world, table, &root_pair, &tmpdiff);
            flecs_table_diff_build_append_table(world, &diff, &tmpdiff);

            dst = flecs_table_traverse_add(world, dst, &tgt_pair, &tmpdiff);
            flecs_table_diff_build_append_table(world, &diff, &tmpdiff);

            if (!desc->lose_depth) {
                if (!has_tgt) {
                    dst = flecs_table_traverse_add(world, dst, &depth_pair, &tmpdiff);
                    flecs_table_diff_build_append_table(world, &diff, &tmpdiff);
                } else {
                    int32_t cur_depth = flecs_depth_for_flat_table(world, table);
                    cur_depth += depth;
                    ecs_entity_t e_depth = flecs_id_for_depth(world, cur_depth);
                    ecs_id_t p_depth = ecs_pair(EcsFlatten, e_depth);
                    dst = flecs_table_traverse_add(world, dst, &p_depth, &tmpdiff);
                    flecs_table_diff_build_append_table(world, &diff, &tmpdiff);
                }
            }

            if (!desc->keep_names) {
                dst = flecs_table_traverse_remove(world, dst, &name_pair, &tmpdiff);
                flecs_table_diff_build_append_table(world, &diff, &tmpdiff);
            }

            int32_t dst_count = ecs_table_count(dst);

            ecs_table_diff_t td;
            flecs_table_diff_build_noalloc(&diff, &td);
            flecs_notify_on_remove(world, table, NULL, 0, count, &td.removed);
            flecs_table_merge(world, dst, table, &dst->data, &table->data);
            flecs_notify_on_add(world, dst, NULL, dst_count, count,
                &td.added, 0);
            flecs_table_diff_builder_fini(world, &diff);

            EcsTarget *fh = ecs_table_get_id(world, dst, tgt_pair, 0);
            ecs_assert(fh != NULL, ECS_INTERNAL_ERROR, NULL);
            ecs_assert(count != 0, ECS_INTERNAL_ERROR, NULL);
            int32_t remain = count;

            for (i = dst_count; i < (dst_count + count); i ++) {
                if (!has_tgt) {
                    fh[i].target = flecs_entities_get_any(world, 
                        ECS_PAIR_SECOND(pair));
                    fh[i].count = remain;
                    remain --;
                }
                ecs_assert(fh[i].target != NULL, ECS_INTERNAL_ERROR, NULL);
            }
        }
    }

    ecs_delete_with(world, pair);
    flecs_id_record_release(world, idr);
}

void ecs_flatten(
    ecs_world_t *world,
    ecs_id_t pair,
    const ecs_flatten_desc_t *desc)
{
    ecs_poly_assert(world, ecs_world_t);
    ecs_entity_t rel = ECS_PAIR_FIRST(pair);
    ecs_entity_t root = ecs_pair_second(world, pair);
    ecs_flatten_desc_t private_desc = {0};
    if (desc) {
        private_desc = *desc;
    }
    
    ecs_run_aperiodic(world, 0);
    ecs_defer_begin(world);

    ecs_id_record_t *idr = flecs_id_record_get(world, pair);

    ecs_table_cache_iter_t it;
    if (idr && flecs_table_cache_iter((ecs_table_cache_t*)idr, &it)) {
        const ecs_table_record_t *tr;
        while ((tr = flecs_table_cache_next(&it, ecs_table_record_t))) {
            ecs_table_t *table = tr->hdr.table;
            if (!table->_->traversable_count) {
                continue;
            }

            if (table->flags & EcsTableIsPrefab) {
                continue;
            }

            int32_t i, count = ecs_table_count(table);
            ecs_record_t **records = table->data.records.array;
            ecs_entity_t *entities = table->data.entities.array;
            for (i = 0; i < count; i ++) {
                ecs_flags32_t flags = ECS_RECORD_TO_ROW_FLAGS(records[i]->row);
                if (flags & EcsEntityIsTarget) {
                    flecs_flatten(world, root, ecs_pair(rel, entities[i]), 1, 
                        &private_desc);
                }
            }
        }
    }

    ecs_defer_end(world);
}

static
const char* flecs_get_identifier(
    const ecs_world_t *world,
    ecs_entity_t entity,
    ecs_entity_t tag)
{
    ecs_check(world != NULL, ECS_INVALID_PARAMETER, NULL);
    ecs_check(ecs_is_alive(world, entity), ECS_INVALID_PARAMETER, NULL);

    const EcsIdentifier *ptr = ecs_get_pair(
        world, entity, EcsIdentifier, tag);

    if (ptr) {
        return ptr->value;
    } else {
        return NULL;
    }
error:
    return NULL;
}

const char* ecs_get_name(
    const ecs_world_t *world,
    ecs_entity_t entity)
{
    return flecs_get_identifier(world, entity, EcsName);
}

const char* ecs_get_symbol(
    const ecs_world_t *world,
    ecs_entity_t entity)
{
    world = ecs_get_world(world);
    if (ecs_owns_pair(world, entity, ecs_id(EcsIdentifier), EcsSymbol)) {
        return flecs_get_identifier(world, entity, EcsSymbol);
    } else {
        return NULL;
    }
}

static
ecs_entity_t flecs_set_identifier(
    ecs_world_t *world,
    ecs_stage_t *stage,
    ecs_entity_t entity,
    ecs_entity_t tag,
    const char *name)
{
    ecs_check(world != NULL, ECS_INVALID_PARAMETER, NULL);
    ecs_check(entity != 0 || name != NULL, ECS_INVALID_PARAMETER, NULL);

    if (!entity) {
        entity = ecs_new_id(world);
    }

    if (!name) {
        ecs_remove_pair(world, entity, ecs_id(EcsIdentifier), tag);
        return entity;
    }

    EcsIdentifier *ptr = ecs_get_mut_pair(world, entity, EcsIdentifier, tag);
    ecs_assert(ptr != NULL, ECS_INTERNAL_ERROR, NULL);

    if (tag == EcsName) {
        /* Insert command after get_mut, but before the name is potentially 
         * freed. Even though the name is a const char*, it is possible that the
         * application passed in the existing name of the entity which could 
         * still cause it to be freed. */
        flecs_defer_path(stage, 0, entity, name);
    }

    ecs_os_strset(&ptr->value, name);
    ecs_modified_pair(world, entity, ecs_id(EcsIdentifier), tag);
    
    return entity;
error:
    return 0;
}

ecs_entity_t ecs_set_name(
    ecs_world_t *world,
    ecs_entity_t entity,
    const char *name)
{
    if (!entity) {
        return ecs_entity(world, {
            .name = name
        });
    }

    ecs_stage_t *stage = flecs_stage_from_world(&world);
    flecs_set_identifier(world, stage, entity, EcsName, name);

    return entity;
}

ecs_entity_t ecs_set_symbol(
    ecs_world_t *world,
    ecs_entity_t entity,
    const char *name)
{
    return flecs_set_identifier(world, NULL, entity, EcsSymbol, name);
}

void ecs_set_alias(
    ecs_world_t *world,
    ecs_entity_t entity,
    const char *name)
{
    flecs_set_identifier(world, NULL, entity, EcsAlias, name);
}

ecs_id_t ecs_make_pair(
    ecs_entity_t relationship,
    ecs_entity_t target)
{
    return ecs_pair(relationship, target);
}

bool ecs_is_valid(
    const ecs_world_t *world,
    ecs_entity_t entity)
{
    ecs_check(world != NULL, ECS_INVALID_PARAMETER, NULL);

    /* 0 is not a valid entity id */
    if (!entity) {
        return false;
    }
    
    /* Make sure we're not working with a stage */
    world = ecs_get_world(world);
    
    /* Entity identifiers should not contain flag bits */
    if (entity & ECS_ID_FLAGS_MASK) {
        return false;
    }

    /* Entities should not contain data in dead zone bits */
    if (entity & ~0xFF00FFFFFFFFFFFF) {
        return false;
    }

    if (entity & ECS_ID_FLAG_BIT) {
        return ecs_entity_t_lo(entity) != 0;
    }

    /* If entity doesn't exist in the world, the id is valid as long as the
     * generation is 0. Using a non-existing id with a non-zero generation
     * requires calling ecs_ensure first. */
    if (!ecs_exists(world, entity)) {
        return ECS_GENERATION(entity) == 0;
    }

    /* If id exists, it must be alive (the generation count must match) */
    return ecs_is_alive(world, entity);
error:
    return false;
}

ecs_id_t ecs_strip_generation(
    ecs_entity_t e)
{
    /* If this is not a pair, erase the generation bits */
    if (!(e & ECS_ID_FLAGS_MASK)) {
        e &= ~ECS_GENERATION_MASK;
    }

    return e;
}

bool ecs_is_alive(
    const ecs_world_t *world,
    ecs_entity_t entity)
{
    ecs_check(world != NULL, ECS_INVALID_PARAMETER, NULL);
    ecs_check(entity != 0, ECS_INVALID_PARAMETER, NULL);

    world = ecs_get_world(world);

    return flecs_entities_is_alive(world, entity);
error:
    return false;
}

ecs_entity_t ecs_get_alive(
    const ecs_world_t *world,
    ecs_entity_t entity)
{
    ecs_check(world != NULL, ECS_INVALID_PARAMETER, NULL);
    
    if (!entity) {
        return 0;
    }

    /* Make sure we're not working with a stage */
    world = ecs_get_world(world);

    if (flecs_entities_is_alive(world, entity)) {
        return entity;
    }

    /* Make sure id does not have generation. This guards against accidentally
     * "upcasting" a not alive identifier to a alive one. */
    if ((uint32_t)entity != entity) {
        return 0;
    }

    ecs_entity_t current = flecs_entities_get_generation(world, entity);
    if (!current || !flecs_entities_is_alive(world, current)) {
        return 0;
    }

    return current;
error:
    return 0;
}

void ecs_ensure(
    ecs_world_t *world,
    ecs_entity_t entity)
{
    ecs_assert(world != NULL, ECS_INTERNAL_ERROR, NULL);
    ecs_check(entity != 0, ECS_INVALID_PARAMETER, NULL);

    /* Const cast is safe, function checks for threading */
    world = (ecs_world_t*)ecs_get_world(world);

    /* The entity index can be mutated while in staged/readonly mode, as long as
     * the world is not multithreaded. */
    ecs_assert(!(world->flags & EcsWorldMultiThreaded), 
        ECS_INVALID_OPERATION, NULL);

    /* Check if a version of the provided id is alive */
    ecs_entity_t any = ecs_get_alive(world, (uint32_t)entity);
    if (any == entity) {
        /* If alive and equal to the argument, there's nothing left to do */
        return;
    }

    /* If the id is currently alive but did not match the argument, fail */
    ecs_check(!any, ECS_INVALID_PARAMETER, NULL);

    /* Set generation if not alive. The sparse set checks if the provided
     * id matches its own generation which is necessary for alive ids. This
     * check would cause ecs_ensure to fail if the generation of the 'entity'
     * argument doesn't match with its generation.
     * 
     * While this could've been addressed in the sparse set, this is a rare
     * scenario that can only be triggered by ecs_ensure. Implementing it here
     * allows the sparse set to not do this check, which is more efficient. */
    flecs_entities_set_generation(world, entity);

    /* Ensure id exists. The underlying datastructure will verify that the
     * generation count matches the provided one. */
    flecs_entities_ensure(world, entity);
error:
    return;
}

void ecs_ensure_id(
    ecs_world_t *world,
    ecs_id_t id)
{
    ecs_poly_assert(world, ecs_world_t);
    if (ECS_HAS_ID_FLAG(id, PAIR)) {
        ecs_entity_t r = ECS_PAIR_FIRST(id);
        ecs_entity_t o = ECS_PAIR_SECOND(id);

        ecs_check(r != 0, ECS_INVALID_PARAMETER, NULL);
        ecs_check(o != 0, ECS_INVALID_PARAMETER, NULL);

        if (flecs_entities_get_generation(world, r) == 0) {
            ecs_assert(!ecs_exists(world, r), ECS_INVALID_PARAMETER, 
                "first element of pair is not alive");
            flecs_entities_ensure(world, r);
        }
        if (flecs_entities_get_generation(world, o) == 0) {
            ecs_assert(!ecs_exists(world, o), ECS_INVALID_PARAMETER,
                "second element of pair is not alive");
            flecs_entities_ensure(world, o);
        }
    } else {
        flecs_entities_ensure(world, id & ECS_COMPONENT_MASK);
    }
error:
    return;
}

bool ecs_exists(
    const ecs_world_t *world,
    ecs_entity_t entity)
{
    ecs_check(world != NULL, ECS_INVALID_PARAMETER, NULL);
    ecs_check(entity != 0, ECS_INVALID_PARAMETER, NULL);

    world = ecs_get_world(world);

    return flecs_entities_exists(world, entity);
error:
    return false;
}

ecs_table_t* ecs_get_table(
    const ecs_world_t *world,
    ecs_entity_t entity)
{
    ecs_check(world != NULL, ECS_INVALID_PARAMETER, NULL);
    ecs_check(ecs_is_valid(world, entity), ECS_INVALID_PARAMETER, NULL);
    
    world = ecs_get_world(world);

    ecs_record_t *record = flecs_entities_get(world, entity);
    ecs_assert(record != NULL, ECS_INTERNAL_ERROR, NULL);
    return record->table;
error:
    return NULL;
}

const ecs_type_t* ecs_get_type(
    const ecs_world_t *world,
    ecs_entity_t entity)
{
    ecs_table_t *table = ecs_get_table(world, entity);
    if (table) {
        return &table->type;
    }

    return NULL;
}

const ecs_type_info_t* ecs_get_type_info(
    const ecs_world_t *world,
    ecs_id_t id)
{
    ecs_check(world != NULL, ECS_INVALID_PARAMETER, NULL);

    world = ecs_get_world(world);

    ecs_id_record_t *idr = flecs_id_record_get(world, id);
    if (!idr && ECS_IS_PAIR(id)) {
        idr = flecs_id_record_get(world, 
            ecs_pair(ECS_PAIR_FIRST(id), EcsWildcard));
        if (!idr || !idr->type_info) {
            idr = NULL;
        }
        if (!idr) {
            ecs_entity_t first = ecs_pair_first(world, id);
            if (!first || !ecs_has_id(world, first, EcsTag)) {
                idr = flecs_id_record_get(world, 
                    ecs_pair(EcsWildcard, ECS_PAIR_SECOND(id)));
                if (!idr || !idr->type_info) {
                    idr = NULL;
                }
            }
        }
    }

    if (idr) {
        return idr->type_info;
    } else if (!(id & ECS_ID_FLAGS_MASK)) {
        return flecs_type_info_get(world, id);
    }
error:
    return NULL;
}

ecs_entity_t ecs_get_typeid(
    const ecs_world_t *world,
    ecs_id_t id)
{
    ecs_check(world != NULL, ECS_INVALID_PARAMETER, NULL);
    const ecs_type_info_t *ti = ecs_get_type_info(world, id);
    if (ti) {
        return ti->component;
    }
error:
    return 0;
}

bool ecs_id_is_tag(
    const ecs_world_t *world,
    ecs_id_t id)
{
    if (ecs_id_is_wildcard(id)) {
        /* If id is a wildcard, we can't tell if it's a tag or not, except
         * when the relationship part of a pair has the Tag property */
        if (ECS_HAS_ID_FLAG(id, PAIR)) {
            if (ECS_PAIR_FIRST(id) != EcsWildcard) {
                ecs_entity_t rel = ecs_pair_first(world, id);
                if (ecs_is_valid(world, rel)) {
                    if (ecs_has_id(world, rel, EcsTag)) {
                        return true;
                    }
                } else {
                    /* During bootstrap it's possible that not all ids are valid
                     * yet. Using ecs_get_typeid will ensure correct values are
                     * returned for only those components initialized during
                     * bootstrap, while still asserting if another invalid id
                     * is provided. */
                    if (ecs_get_typeid(world, id) == 0) {
                        return true;
                    }
                }
            } else {
                /* If relationship is wildcard id is not guaranteed to be a tag */
            }
        }
    } else {
        if (ecs_get_typeid(world, id) == 0) {
            return true;
        }
    }

    return false;
}

bool ecs_id_is_union(
    const ecs_world_t *world,
    ecs_id_t id)
{
    if (!ECS_IS_PAIR(id)) {
        return false;
    } else if (ECS_PAIR_FIRST(id) == EcsUnion) {
        return true;
    } else {
        ecs_entity_t first = ecs_pair_first(world, id);
        if (ecs_has_id(world, first, EcsUnion)) {
            return true;
        }
    }

    return false;
}

int32_t ecs_count_id(
    const ecs_world_t *world,
    ecs_entity_t id)
{
    ecs_check(world != NULL, ECS_INVALID_PARAMETER, NULL);

    if (!id) {
        return 0;
    }

    int32_t count = 0;
    ecs_iter_t it = ecs_term_iter(world, &(ecs_term_t) { 
        .id = id,
        .src.flags = EcsSelf
    });

    it.flags |= EcsIterNoData;
    it.flags |= EcsIterEvalTables;

    while (ecs_term_next(&it)) {
        count += it.count;
    }

    return count;
error:
    return 0;
}

void ecs_enable(
    ecs_world_t *world,
    ecs_entity_t entity,
    bool enabled)
{
    if (ecs_has_id(world, entity, EcsPrefab)) {
        /* If entity is a type, enable/disable all entities in the type */
        const ecs_type_t *type = ecs_get_type(world, entity);
        ecs_assert(type != NULL, ECS_INTERNAL_ERROR, NULL);
        ecs_id_t *ids = type->array;
        int32_t i, count = type->count;
        for (i = 0; i < count; i ++) {
            ecs_id_t id = ids[i];
            if (ecs_id_get_flags(world, id) & EcsIdDontInherit) {
                continue;
            }
            ecs_enable(world, id, enabled);
        }
    } else {
        if (enabled) {
            ecs_remove_id(world, entity, EcsDisabled);
        } else {
            ecs_add_id(world, entity, EcsDisabled);
        }
    }
}

bool ecs_defer_begin(
    ecs_world_t *world)
{
    ecs_check(world != NULL, ECS_INVALID_PARAMETER, NULL);
    ecs_stage_t *stage = flecs_stage_from_world(&world);
    return flecs_defer_begin(world, stage);
error:
    return false;
}

bool ecs_defer_end(
    ecs_world_t *world)
{
    ecs_check(world != NULL, ECS_INVALID_PARAMETER, NULL);
    ecs_stage_t *stage = flecs_stage_from_world(&world);
    return flecs_defer_end(world, stage);
error:
    return false;
}

void ecs_defer_suspend(
    ecs_world_t *world)
{
    ecs_check(world != NULL, ECS_INVALID_PARAMETER, NULL);
    ecs_check(ecs_is_deferred(world), ECS_INVALID_OPERATION, NULL);
    ecs_stage_t *stage = flecs_stage_from_world(&world);
    ecs_check(stage->defer > 0, ECS_INVALID_OPERATION, NULL);
    stage->defer = -stage->defer;
error:
    return;
}

void ecs_defer_resume(
    ecs_world_t *world)
{
    ecs_check(world != NULL, ECS_INVALID_PARAMETER, NULL);
    ecs_stage_t *stage = flecs_stage_from_world(&world);
    ecs_check(stage->defer < 0, ECS_INVALID_OPERATION, NULL);
    stage->defer = -stage->defer;
error:
    return;
}

const char* ecs_id_flag_str(
    ecs_entity_t entity)
{
    if (ECS_HAS_ID_FLAG(entity, PAIR)) {
        return "PAIR";
    } else
    if (ECS_HAS_ID_FLAG(entity, TOGGLE)) {
        return "TOGGLE";
    } else
    if (ECS_HAS_ID_FLAG(entity, AND)) {
        return "AND";
    } else
    if (ECS_HAS_ID_FLAG(entity, OVERRIDE)) {
        return "OVERRIDE";
    } else {
        return "UNKNOWN";
    }
}

void ecs_id_str_buf(
    const ecs_world_t *world,
    ecs_id_t id,
    ecs_strbuf_t *buf)
{
    ecs_check(world != NULL, ECS_INVALID_PARAMETER, NULL);

    world = ecs_get_world(world);

    if (ECS_HAS_ID_FLAG(id, TOGGLE)) {
        ecs_strbuf_appendstr(buf, ecs_id_flag_str(ECS_TOGGLE));
        ecs_strbuf_appendch(buf, '|');
    }

    if (ECS_HAS_ID_FLAG(id, OVERRIDE)) {
        ecs_strbuf_appendstr(buf, ecs_id_flag_str(ECS_OVERRIDE));
        ecs_strbuf_appendch(buf, '|');
    }

    if (ECS_HAS_ID_FLAG(id, AND)) {
        ecs_strbuf_appendstr(buf, ecs_id_flag_str(ECS_AND));
        ecs_strbuf_appendch(buf, '|');
    }

    if (ECS_HAS_ID_FLAG(id, PAIR)) {
        ecs_entity_t rel = ECS_PAIR_FIRST(id);
        ecs_entity_t obj = ECS_PAIR_SECOND(id);

        ecs_entity_t e;
        if ((e = ecs_get_alive(world, rel))) {
            rel = e;
        }
        if ((e = ecs_get_alive(world, obj))) {
            obj = e;
        }

        ecs_strbuf_appendch(buf, '(');
        ecs_get_path_w_sep_buf(world, 0, rel, NULL, NULL, buf);
        ecs_strbuf_appendch(buf, ',');
        ecs_get_path_w_sep_buf(world, 0, obj, NULL, NULL, buf);
        ecs_strbuf_appendch(buf, ')');
    } else {
        ecs_entity_t e = id & ECS_COMPONENT_MASK;
        ecs_get_path_w_sep_buf(world, 0, e, NULL, NULL, buf);
    }

error:
    return;
}

char* ecs_id_str(
    const ecs_world_t *world,
    ecs_id_t id)
{
    ecs_strbuf_t buf = ECS_STRBUF_INIT;
    ecs_id_str_buf(world, id, &buf);
    return ecs_strbuf_get(&buf);
}

static
void ecs_type_str_buf(
    const ecs_world_t *world,
    const ecs_type_t *type,
    ecs_strbuf_t *buf)
{
    ecs_entity_t *ids = type->array;
    int32_t i, count = type->count;

    for (i = 0; i < count; i ++) {
        ecs_entity_t id = ids[i];

        if (i) {
            ecs_strbuf_appendch(buf, ',');
            ecs_strbuf_appendch(buf, ' ');
        }

        if (id == 1) {
            ecs_strbuf_appendlit(buf, "Component");
        } else {
            ecs_id_str_buf(world, id, buf);
        }
    }
}

char* ecs_type_str(
    const ecs_world_t *world,
    const ecs_type_t *type)
{
    if (!type) {
        return ecs_os_strdup("");
    }

    ecs_strbuf_t buf = ECS_STRBUF_INIT;
    ecs_type_str_buf(world, type, &buf);
    return ecs_strbuf_get(&buf);
}

char* ecs_table_str(
    const ecs_world_t *world,
    const ecs_table_t *table)
{
    if (table) {
        return ecs_type_str(world, &table->type);
    } else {
        return NULL;
    }
}

char* ecs_entity_str(
    const ecs_world_t *world,
    ecs_entity_t entity)
{
    ecs_strbuf_t buf = ECS_STRBUF_INIT;
    ecs_check(ecs_is_alive(world, entity), ECS_INVALID_PARAMETER, NULL);

    ecs_get_path_w_sep_buf(world, 0, entity, 0, "", &buf);
    
    ecs_strbuf_appendlit(&buf, " [");
    const ecs_type_t *type = ecs_get_type(world, entity);
    if (type) {
        ecs_type_str_buf(world, type, &buf);
    }
    ecs_strbuf_appendch(&buf, ']');

    return ecs_strbuf_get(&buf);
error:
    return NULL;
}

static
void flecs_flush_bulk_new(
    ecs_world_t *world,
    ecs_cmd_t *cmd)
{
    ecs_entity_t *entities = cmd->is._n.entities;

    if (cmd->id) {
        int i, count = cmd->is._n.count;
        for (i = 0; i < count; i ++) {
            flecs_entities_ensure(world, entities[i]);
            flecs_add_id(world, entities[i], cmd->id);
        }
    }

    ecs_os_free(entities);
}

static
void flecs_dtor_value(
    ecs_world_t *world,
    ecs_id_t id,
    void *value,
    int32_t count)
{
    const ecs_type_info_t *ti = ecs_get_type_info(world, id);
    ecs_assert(ti != NULL, ECS_INTERNAL_ERROR, NULL);
    ecs_xtor_t dtor = ti->hooks.dtor;
    if (dtor) {
        ecs_size_t size = ti->size;
        void *ptr;
        int i;
        for (i = 0, ptr = value; i < count; i ++, ptr = ECS_OFFSET(ptr, size)) {
            dtor(ptr, 1, ti);
        }
    }
}

static
void flecs_discard_cmd(
    ecs_world_t *world,
    ecs_cmd_t *cmd)
{
    if (cmd->kind != EcsOpBulkNew) {
        void *value = cmd->is._1.value;
        if (value) {
            flecs_dtor_value(world, cmd->id, value, 1);
            flecs_stack_free(value, cmd->is._1.size);
        }
    } else {
        ecs_os_free(cmd->is._n.entities);
    }
}

static
bool flecs_remove_invalid(
    ecs_world_t *world,
    ecs_id_t id,
    ecs_id_t *id_out)
{
    if (ECS_HAS_ID_FLAG(id, PAIR)) {
        ecs_entity_t rel = ECS_PAIR_FIRST(id);
        if (!flecs_entities_is_valid(world, rel)) {
            /* After relationship is deleted we can no longer see what its
             * delete action was, so pretend this never happened */
            *id_out = 0;
            return true;
        } else {
            ecs_entity_t obj = ECS_PAIR_SECOND(id);
            if (!flecs_entities_is_valid(world, obj)) {
                /* Check the relationship's policy for deleted objects */
                ecs_id_record_t *idr = flecs_id_record_get(world, 
                    ecs_pair(rel, EcsWildcard));
                if (idr) {
                    ecs_entity_t action = ECS_ID_ON_DELETE_OBJECT(idr->flags);
                    if (action == EcsDelete) {
                        /* Entity should be deleted, don't bother checking
                         * other ids */
                        return false;
                    } else if (action == EcsPanic) {
                        /* If policy is throw this object should not have
                         * been deleted */
                        flecs_throw_invalid_delete(world, id);
                    } else {
                        *id_out = 0;
                        return true;
                    }
                } else {
                    *id_out = 0;
                    return true;
                }
            }
        }
    } else {
        id &= ECS_COMPONENT_MASK;
        if (!flecs_entities_is_valid(world, id)) {
            /* After relationship is deleted we can no longer see what its
             * delete action was, so pretend this never happened */
            *id_out = 0;
            return true;
        }
    }

    return true;
}

static
void flecs_cmd_batch_for_entity(
    ecs_world_t *world,
    ecs_table_diff_builder_t *diff,
    ecs_entity_t entity,
    ecs_cmd_t *cmds,
    int32_t start)
{
    ecs_record_t *r = flecs_entities_get(world, entity);
    ecs_table_t *table = NULL;
    if (r) {
        table = r->table;
    }

    world->info.cmd.batched_entity_count ++;

    ecs_table_t *start_table = table;
    ecs_cmd_t *cmd;
    int32_t next_for_entity;
    ecs_table_diff_t table_diff; /* Keep track of diff for observers/hooks */
    int32_t cur = start;
    ecs_id_t id;
    bool has_set = false;

    do {
        cmd = &cmds[cur];
        id = cmd->id;
        next_for_entity = cmd->next_for_entity;
        if (next_for_entity < 0) {
            /* First command for an entity has a negative index, flip sign */
            next_for_entity *= -1;
        }

        /* Check if added id is still valid (like is the parent of a ChildOf 
         * pair still alive), if not run cleanup actions for entity */
        if (id) {
            if (flecs_remove_invalid(world, id, &id)) {
                if (!id) {
                    /* Entity should remain alive but id should not be added */
                    cmd->kind = EcsOpSkip;
                    continue;
                }
                /* Entity should remain alive and id is still valid */
            } else {
                /* Id was no longer valid and had a Delete policy */
                cmd->kind = EcsOpSkip;
                ecs_delete(world, entity);
                flecs_table_diff_builder_clear(diff);
                return;
            }
        }

        ecs_cmd_kind_t kind = cmd->kind;
        switch(kind) {
        case EcsOpAddModified:
            /* Add is batched, but keep Modified */
            cmd->kind = EcsOpModified;

            /* fallthrough */
        case EcsOpAdd:
            table = flecs_find_table_add(world, table, id, diff);
            world->info.cmd.batched_command_count ++;
            break;
        case EcsOpModified: {
            if (start_table) {
                /* If a modified was inserted for an existing component, the value
                 * of the component could have been changed. If this is the case,
                 * call on_set hooks before the OnAdd/OnRemove observers are invoked
                 * when moving the entity to a different table.
                 * This ensures that if OnAdd/OnRemove observers access the modified
                 * component value, the on_set hook has had the opportunity to
                 * run first to set any computed values of the component. */
                int32_t row = ECS_RECORD_TO_ROW(r->row);
                flecs_component_ptr_t ptr = flecs_get_component_ptr(
                    world, start_table, row, cmd->id);
                if (ptr.ptr) {
                    ecs_type_info_t *ti = ptr.ti;
                    ecs_iter_action_t on_set;
                    if ((on_set = ti->hooks.on_set)) {
                        flecs_invoke_hook(world, start_table, 1, row, &entity,
                            ptr.ptr, cmd->id, ptr.ti, EcsOnSet, on_set);
                    }
                }
            }
            break;
        }
        case EcsOpSet:
        case EcsOpMut:
            table = flecs_find_table_add(world, table, id, diff);
            world->info.cmd.batched_command_count ++;
            has_set = true;
            break;
        case EcsOpEmplace:
            /* Don't add for emplace, as this requires a special call to ensure
             * the constructor is not invoked for the component */
            break;
        case EcsOpRemove:
            table = flecs_find_table_remove(world, table, id, diff);
            world->info.cmd.batched_command_count ++;
            break;
        case EcsOpClear:
            if (table) {
                ecs_id_t *ids = ecs_vec_grow_t(&world->allocator, 
                    &diff->removed, ecs_id_t, table->type.count);
                ecs_os_memcpy_n(ids, table->type.array, ecs_id_t, 
                    table->type.count);
            }
            table = &world->store.root;
            world->info.cmd.batched_command_count ++;
            break;
        default:
            break;
        }

        /* Add, remove and clear operations can be skipped since they have no
         * side effects besides adding/removing components */
        if (kind == EcsOpAdd || kind == EcsOpRemove || kind == EcsOpClear) {
            cmd->kind = EcsOpSkip;
        }
    } while ((cur = next_for_entity));

    /* Move entity to destination table in single operation */
    flecs_table_diff_build_noalloc(diff, &table_diff);
    flecs_defer_begin(world, &world->stages[0]);
    flecs_commit(world, entity, r, table, &table_diff, true, 0);
    flecs_defer_end(world, &world->stages[0]);
    flecs_table_diff_builder_clear(diff);

    /* If ids were both removed and set, check if there are ids that were both
     * set and removed. If so, skip the set command so that the id won't get
     * re-added */
    if (has_set && table_diff.removed.count) {
        cur = start;
        do {
            cmd = &cmds[cur];
            next_for_entity = cmd->next_for_entity;
            if (next_for_entity < 0) {
                next_for_entity *= -1;
            }
            switch(cmd->kind) {
            case EcsOpSet:
            case EcsOpMut: {
                ecs_id_record_t *idr = cmd->idr;
                if (!idr) {
                    idr = flecs_id_record_get(world, cmd->id);
                }

                if (!flecs_id_record_get_table(idr, table)) {
                    /* Component was deleted */
                    cmd->kind = EcsOpSkip;
                    world->info.cmd.batched_command_count --;
                    world->info.cmd.discard_count ++;
                }
                break;
            }
            default:
                break;
            }
        } while ((cur = next_for_entity));
    }
}

/* Leave safe section. Run all deferred commands. */
bool flecs_defer_end(
    ecs_world_t *world,
    ecs_stage_t *stage)
{
    ecs_poly_assert(world, ecs_world_t);
    ecs_poly_assert(stage, ecs_stage_t);

    if (stage->defer < 0) {
        /* Defer suspending makes it possible to do operations on the storage
         * without flushing the commands in the queue */
        return false;
    }

    ecs_assert(stage->defer > 0, ECS_INTERNAL_ERROR, NULL);

    if (!--stage->defer) {
        /* Test whether we're flushing to another queue or whether we're 
         * flushing to the storage */
        bool merge_to_world = false;
        if (ecs_poly_is(world, ecs_world_t)) {
            merge_to_world = world->stages[0].defer == 0;
        }

        ecs_stage_t *dst_stage = flecs_stage_from_world(&world);
        if (ecs_vec_count(&stage->commands)) {
            ecs_vec_t commands = stage->commands;
            stage->commands.array = NULL;
            stage->commands.count = 0;
            stage->commands.size = 0;

            ecs_cmd_t *cmds = ecs_vec_first(&commands);
            int32_t i, count = ecs_vec_count(&commands);
            ecs_vec_init_t(NULL, &stage->commands, ecs_cmd_t, 0);

            ecs_stack_t stack = stage->defer_stack;
            flecs_stack_init(&stage->defer_stack);

            ecs_table_diff_builder_t diff;
            flecs_table_diff_builder_init(world, &diff);
            flecs_sparse_clear(&stage->cmd_entries);

            for (i = 0; i < count; i ++) {
                ecs_cmd_t *cmd = &cmds[i];
                ecs_entity_t e = cmd->entity;
                bool is_alive = flecs_entities_is_valid(world, e);

                /* A negative index indicates the first command for an entity */
                if (merge_to_world && (cmd->next_for_entity < 0)) {
                    /* Batch commands for entity to limit archetype moves */
                    if (is_alive) {
                        flecs_cmd_batch_for_entity(world, &diff, e, cmds, i);
                    } else {
                        world->info.cmd.discard_count ++;
                    }
                }

                /* If entity is no longer alive, this could be because the queue
                 * contained both a delete and a subsequent add/remove/set which
                 * should be ignored. */
                ecs_cmd_kind_t kind = cmd->kind;
                if ((kind != EcsOpPath) && ((kind == EcsOpSkip) || (e && !is_alive))) {
                    world->info.cmd.discard_count ++;
                    flecs_discard_cmd(world, cmd);
                    continue;
                }

                ecs_id_t id = cmd->id;

                switch(kind) {
                case EcsOpAdd:
                    ecs_assert(id != 0, ECS_INTERNAL_ERROR, NULL);
                    if (flecs_remove_invalid(world, id, &id)) {
                        if (id) {
                            world->info.cmd.add_count ++;
                            flecs_add_id(world, e, id);
                        } else {
                            world->info.cmd.discard_count ++;
                        }
                    } else {
                        world->info.cmd.discard_count ++;
                        ecs_delete(world, e);
                    }
                    break;
                case EcsOpRemove:
                    flecs_remove_id(world, e, id);
                    world->info.cmd.remove_count ++;
                    break;
                case EcsOpClone:
                    ecs_clone(world, e, id, cmd->is._1.clone_value);
                    break;
                case EcsOpSet:
                    flecs_move_ptr_w_id(world, dst_stage, e, 
                        cmd->id, flecs_itosize(cmd->is._1.size), 
                        cmd->is._1.value, kind);
                    world->info.cmd.set_count ++;
                    break;
                case EcsOpEmplace:
                    if (merge_to_world) {
                        ecs_emplace_id(world, e, id);
                    }
                    flecs_move_ptr_w_id(world, dst_stage, e, 
                        cmd->id, flecs_itosize(cmd->is._1.size), 
                        cmd->is._1.value, kind);
                    world->info.cmd.get_mut_count ++;
                    break;
                case EcsOpMut:
                    flecs_move_ptr_w_id(world, dst_stage, e, 
                        cmd->id, flecs_itosize(cmd->is._1.size), 
                        cmd->is._1.value, kind);
                    world->info.cmd.get_mut_count ++;
                    break;
                case EcsOpModified:
                    flecs_modified_id_if(world, e, id);
                    world->info.cmd.modified_count ++;
                    break;
                case EcsOpAddModified:
                    flecs_add_id(world, e, id);
                    flecs_modified_id_if(world, e, id);
                    world->info.cmd.add_count ++;
                    world->info.cmd.modified_count ++;
                    break;
                case EcsOpDelete: {
                    ecs_delete(world, e);
                    world->info.cmd.delete_count ++;
                    break;
                }
                case EcsOpClear:
                    ecs_clear(world, e);
                    world->info.cmd.clear_count ++;
                    break;
                case EcsOpOnDeleteAction:
                    ecs_defer_begin(world);
                    flecs_on_delete(world, id, e, false);
                    ecs_defer_end(world);
                    world->info.cmd.other_count ++;
                    break;
                case EcsOpEnable:
                    ecs_enable_id(world, e, id, true);
                    world->info.cmd.other_count ++;
                    break;
                case EcsOpDisable:
                    ecs_enable_id(world, e, id, false);
                    world->info.cmd.other_count ++;
                    break;
                case EcsOpBulkNew:
                    flecs_flush_bulk_new(world, cmd);
                    world->info.cmd.other_count ++;
                    continue;
                case EcsOpPath:
                    ecs_ensure(world, e);
                    if (cmd->id) {
                        ecs_add_pair(world, e, EcsChildOf, cmd->id);
                    }
                    ecs_set_name(world, e, cmd->is._1.value);
                    ecs_os_free(cmd->is._1.value);
                    cmd->is._1.value = NULL;
                    break;
                case EcsOpSkip:
                    break;
                }

                if (cmd->is._1.value) {
                    flecs_stack_free(cmd->is._1.value, cmd->is._1.size);
                }
            }

            ecs_vec_fini_t(&stage->allocator, &stage->commands, ecs_cmd_t);

            /* Restore defer queue */
            ecs_vec_clear(&commands);
            stage->commands = commands;

            /* Restore stack */
            flecs_stack_fini(&stage->defer_stack);
            stage->defer_stack = stack;
            flecs_stack_reset(&stage->defer_stack);

            flecs_table_diff_builder_fini(world, &diff);
        }

        return true;
    }

    return false;
}

/* Delete operations from queue without executing them. */
bool flecs_defer_purge(
    ecs_world_t *world,
    ecs_stage_t *stage)
{
    ecs_check(world != NULL, ECS_INVALID_PARAMETER, NULL);
    ecs_check(stage != NULL, ECS_INVALID_PARAMETER, NULL);

    if (!--stage->defer) {
        ecs_vec_t commands = stage->commands;

        if (ecs_vec_count(&commands)) {
            ecs_cmd_t *cmds = ecs_vec_first(&commands);
            int32_t i, count = ecs_vec_count(&commands);
            for (i = 0; i < count; i ++) {
                flecs_discard_cmd(world, &cmds[i]);
            }

            ecs_vec_fini_t(&stage->allocator, &stage->commands, ecs_cmd_t);

            ecs_vec_clear(&commands);
            flecs_stack_reset(&stage->defer_stack);
            flecs_sparse_clear(&stage->cmd_entries);
        }

        return true;
    }

error:
    return false;
}

/**
 * @file stage.c
 * @brief Staging implementation.
 * 
 * A stage is an object that can be used to temporarily store mutations to a
 * world while a world is in readonly mode. ECS operations that are invoked on
 * a stage are stored in a command buffer, which is flushed during sync points,
 * or manually by the user.
 * 
 * Stages contain additional state to enable other API functionality without
 * having to mutate the world, such as setting the current scope, and allocators
 * that are local to a stage.
 * 
 * In a multi threaded application, each thread has its own stage which allows
 * threads to insert mutations without having to lock administration.
 */


static
ecs_cmd_t* flecs_cmd_alloc(
    ecs_stage_t *stage)
{
    ecs_cmd_t *cmd = ecs_vec_append_t(&stage->allocator, &stage->commands, 
        ecs_cmd_t);
    ecs_os_zeromem(cmd);
    return cmd;
}

static
ecs_cmd_t* flecs_cmd_new(
    ecs_stage_t *stage, 
    ecs_entity_t e, 
    bool is_delete,
    bool can_batch) 
{
    if (e) {
        ecs_vec_t *cmds = &stage->commands;
        ecs_cmd_entry_t *entry = flecs_sparse_try_t(
            &stage->cmd_entries, ecs_cmd_entry_t, e);
        int32_t cur = ecs_vec_count(cmds);
        if (entry) {
            int32_t last = entry->last;
            if (entry->last == -1) {
                /* Entity was deleted, don't insert command */
                return NULL;
            }

            if (can_batch) {
                ecs_cmd_t *arr = ecs_vec_first_t(cmds, ecs_cmd_t);
                ecs_assert(arr[last].entity == e, ECS_INTERNAL_ERROR, NULL);
                ecs_cmd_t *last_op = &arr[last];
                last_op->next_for_entity = cur;
                if (last == entry->first) {
                    /* Flip sign bit so flush logic can tell which command
                     * is the first for an entity */
                    last_op->next_for_entity *= -1;
                }
            }
        } else if (can_batch || is_delete) {
            entry = flecs_sparse_ensure_t(&stage->cmd_entries, 
                ecs_cmd_entry_t, e);
            entry->first = cur;
        }
        if (can_batch) {
            entry->last = cur;
        }
        if (is_delete) {
            /* Prevent insertion of more commands for entity */
            entry->last = -1;
        }
    }

    return flecs_cmd_alloc(stage);
}

static
void flecs_stages_merge(
    ecs_world_t *world,
    bool force_merge)
{
    bool is_stage = ecs_poly_is(world, ecs_stage_t);
    ecs_stage_t *stage = flecs_stage_from_world(&world);

    bool measure_frame_time = ECS_BIT_IS_SET(world->flags, 
        EcsWorldMeasureFrameTime);

    ecs_time_t t_start = {0};
    if (measure_frame_time) {
        ecs_os_get_time(&t_start);
    }

    ecs_dbg_3("#[magenta]merge");
    ecs_log_push_3();

    if (is_stage) {
        /* Check for consistency if force_merge is enabled. In practice this
         * function will never get called with force_merge disabled for just
         * a single stage. */
        if (force_merge || stage->auto_merge) {
            ecs_assert(stage->defer == 1, ECS_INVALID_OPERATION, 
                "mismatching defer_begin/defer_end detected");
            flecs_defer_end(world, stage);
        }
    } else {
        /* Merge stages. Only merge if the stage has auto_merging turned on, or 
         * if this is a forced merge (like when ecs_merge is called) */
        int32_t i, count = ecs_get_stage_count(world);
        for (i = 0; i < count; i ++) {
            ecs_stage_t *s = (ecs_stage_t*)ecs_get_stage(world, i);
            ecs_poly_assert(s, ecs_stage_t);
            if (force_merge || s->auto_merge) {
                flecs_defer_end(world, s);
            }
        }
    }

    flecs_eval_component_monitors(world);

    if (measure_frame_time) {
        world->info.merge_time_total += (ecs_ftime_t)ecs_time_measure(&t_start);
    }

    world->info.merge_count_total ++; 

    /* If stage is asynchronous, deferring is always enabled */
    if (stage->async) {
        flecs_defer_begin(world, stage);
    }
    
    ecs_log_pop_3();
}

static
void flecs_stage_auto_merge(
    ecs_world_t *world)
{
    flecs_stages_merge(world, false);
}

static
void flecs_stage_manual_merge(
    ecs_world_t *world)
{
    flecs_stages_merge(world, true);
}

bool flecs_defer_begin(
    ecs_world_t *world,
    ecs_stage_t *stage)
{
    ecs_poly_assert(world, ecs_world_t);
    ecs_poly_assert(stage, ecs_stage_t);
    (void)world;
    if (stage->defer < 0) return false;
    return (++ stage->defer) == 1;
}

bool flecs_defer_cmd(
    ecs_stage_t *stage)
{
    if (stage->defer) {
        return (stage->defer > 0);
    }

    stage->defer ++;
    return false;
}

bool flecs_defer_modified(
    ecs_stage_t *stage,
    ecs_entity_t entity,
    ecs_id_t id)
{
    if (flecs_defer_cmd(stage)) {
        ecs_cmd_t *cmd = flecs_cmd_new(stage, entity, false, true);
        if (cmd) {
            cmd->kind = EcsOpModified;
            cmd->id = id;
            cmd->entity = entity;
        }
        return true;
    }
    return false;
}

bool flecs_defer_clone(
    ecs_stage_t *stage,
    ecs_entity_t entity,
    ecs_entity_t src,
    bool clone_value)
{   
    if (flecs_defer_cmd(stage)) {
        ecs_cmd_t *cmd = flecs_cmd_new(stage, entity, false, false);
        if (cmd) {
            cmd->kind = EcsOpClone;
            cmd->id = src;
            cmd->entity = entity;
            cmd->is._1.clone_value = clone_value;
        }
        return true;
    }
    return false;   
}

bool flecs_defer_path(
    ecs_stage_t *stage,
    ecs_entity_t parent,
    ecs_entity_t entity,
    const char *name)
{
    if (stage->defer > 0) {
        ecs_cmd_t *cmd = flecs_cmd_new(stage, entity, false, false);
        if (cmd) {
            cmd->kind = EcsOpPath;
            cmd->entity = entity;
            cmd->id = parent;
            cmd->is._1.value = ecs_os_strdup(name);
        }
        return true;
    }
    return false;
}

bool flecs_defer_delete(
    ecs_stage_t *stage,
    ecs_entity_t entity)
{
    if (flecs_defer_cmd(stage)) {
        ecs_cmd_t *cmd = flecs_cmd_new(stage, entity, true, false);
        if (cmd) {
            cmd->kind = EcsOpDelete;
            cmd->entity = entity;
        }
        return true;
    }
    return false;
}

bool flecs_defer_clear(
    ecs_stage_t *stage,
    ecs_entity_t entity)
{
    if (flecs_defer_cmd(stage)) {
        ecs_cmd_t *cmd = flecs_cmd_new(stage, entity, false, true);
        if (cmd) {
            cmd->kind = EcsOpClear;
            cmd->entity = entity;
        }
        return true;
    }
    return false;
}

bool flecs_defer_on_delete_action(
    ecs_stage_t *stage,
    ecs_id_t id,
    ecs_entity_t action)
{
    if (flecs_defer_cmd(stage)) {
        ecs_cmd_t *cmd = flecs_cmd_alloc(stage);
        cmd->kind = EcsOpOnDeleteAction;
        cmd->id = id;
        cmd->entity = action;
        return true;
    }
    return false;
}

bool flecs_defer_enable(
    ecs_stage_t *stage,
    ecs_entity_t entity,
    ecs_id_t id,
    bool enable)
{
    if (flecs_defer_cmd(stage)) {
        ecs_cmd_t *cmd = flecs_cmd_new(stage, entity, false, false);
        if (cmd) {
            cmd->kind = enable ? EcsOpEnable : EcsOpDisable;
            cmd->entity = entity;
            cmd->id = id;
        }
        return true;
    }
    return false;
}

bool flecs_defer_bulk_new(
    ecs_world_t *world,
    ecs_stage_t *stage,
    int32_t count,
    ecs_id_t id,
    const ecs_entity_t **ids_out)
{
    if (flecs_defer_cmd(stage)) {
        ecs_entity_t *ids = ecs_os_malloc(count * ECS_SIZEOF(ecs_entity_t));

        /* Use ecs_new_id as this is thread safe */
        int i;
        for (i = 0; i < count; i ++) {
            ids[i] = ecs_new_id(world);
        }

        *ids_out = ids;

        /* Store data in op */
        ecs_cmd_t *cmd = flecs_cmd_alloc(stage);
        if (cmd) {
            cmd->kind = EcsOpBulkNew;
            cmd->id = id;
            cmd->is._n.entities = ids;
            cmd->is._n.count = count;
        }

        return true;
    }
    return false;
}

bool flecs_defer_add(
    ecs_stage_t *stage,
    ecs_entity_t entity,
    ecs_id_t id)
{
    if (flecs_defer_cmd(stage)) {
        ecs_assert(id != 0, ECS_INTERNAL_ERROR, NULL);
        ecs_cmd_t *cmd = flecs_cmd_new(stage, entity, false, true);
        if (cmd) {
            cmd->kind = EcsOpAdd;
            cmd->id = id;
            cmd->entity = entity;
        }
        return true;
    }
    return false;
}

bool flecs_defer_remove(
    ecs_stage_t *stage,
    ecs_entity_t entity,
    ecs_id_t id)
{
    if (flecs_defer_cmd(stage)) {
        ecs_assert(id != 0, ECS_INTERNAL_ERROR, NULL);
        ecs_cmd_t *cmd = flecs_cmd_new(stage, entity, false, true);
        if (cmd) {
            cmd->kind = EcsOpRemove;
            cmd->id = id;
            cmd->entity = entity;
        }
        return true;
    }
    return false;
}

void* flecs_defer_set(
    ecs_world_t *world,
    ecs_stage_t *stage,
    ecs_cmd_kind_t cmd_kind,
    ecs_entity_t entity,
    ecs_id_t id,
    ecs_size_t size,
    void *value,
    bool need_value)
{
    ecs_cmd_t *cmd = flecs_cmd_new(stage, entity, false, true);
    if (!cmd) {
        if (need_value) {
            /* Entity is deleted by a previous command, but we still need to 
             * return a temporary storage to the application. */
            cmd_kind = EcsOpSkip;
        } else {
            /* No value needs to be returned, we can drop the command */
            return NULL;
        }
    }

    /* Find type info for id */
    const ecs_type_info_t *ti = NULL;
    ecs_id_record_t *idr = flecs_id_record_get(world, id);
    if (!idr) {
        /* If idr doesn't exist yet, create it but only if the 
         * application is not multithreaded. */
        if (stage->async || (world->flags & EcsWorldMultiThreaded)) {
            ti = ecs_get_type_info(world, id);
            ecs_assert(ti != NULL, ECS_INVALID_PARAMETER, NULL);
        } else {
            /* When not in multi threaded mode, it's safe to find or 
             * create the id record. */
            idr = flecs_id_record_ensure(world, id);
            ecs_assert(idr != NULL, ECS_INTERNAL_ERROR, NULL);
            
            /* Get type_info from id record. We could have called 
             * ecs_get_type_info directly, but since this function can be
             * expensive for pairs, creating the id record ensures we can
             * find the type_info quickly for subsequent operations. */
            ti = idr->type_info;
        }
    } else {
        ti = idr->type_info;
    }

    /* If the id isn't associated with a type, we can't set anything */
    ecs_check(ti != NULL, ECS_INVALID_PARAMETER, NULL);

    /* Make sure the size of the value equals the type size */
    ecs_assert(!size || size == ti->size, ECS_INVALID_PARAMETER, NULL);
    size = ti->size;

    /* Find existing component. Make sure it's owned, so that we won't use the
     * component of a prefab. */
    void *existing = NULL;
    ecs_table_t *table = NULL, *storage_table;
    if (idr) {
        /* Entity can only have existing component if id record exists */
        ecs_record_t *r = flecs_entities_get(world, entity);
        table = r->table;
        if (r && table && (storage_table = table->storage_table)) {
            const ecs_table_record_t *tr = flecs_id_record_get_table(
                idr, storage_table);
            if (tr) {
                /* Entity has the component */
                ecs_vec_t *column = &table->data.columns[tr->column];
                existing = ecs_vec_get(column, size, ECS_RECORD_TO_ROW(r->row));
            }
        }
    }

    /* Get existing value from storage */
    void *cmd_value = existing;
    bool emplace = cmd_kind == EcsOpEmplace;

    /* If the component does not yet exist, create a temporary value. This is
     * necessary so we can store a component value in the deferred command,
     * without adding the component to the entity which is not allowed in 
     * deferred mode. */
    if (!existing) {
        ecs_stack_t *stack = &stage->defer_stack;
        cmd_value = flecs_stack_alloc(stack, size, ti->alignment);

        /* If the component doesn't yet exist, construct it and move the 
         * provided value into the component, if provided. Don't construct if
         * this is an emplace operation, in which case the application is
         * responsible for constructing. */
        if (value) {
            if (emplace) {
                ecs_move_t move = ti->hooks.move_ctor;
                if (move) {
                    move(cmd_value, value, 1, ti);
                } else {
                    ecs_os_memcpy(cmd_value, value, size);
                }
            } else {
                ecs_copy_t copy = ti->hooks.copy_ctor;
                if (copy) {
                    copy(cmd_value, value, 1, ti);
                } else {
                    ecs_os_memcpy(cmd_value, value, size);
                }
            }
        } else if (!emplace) {
            /* If the command is not an emplace, construct the temp storage */

            /* Check if entity inherits component */
            void *base = NULL;
            if (table && (table->flags & EcsTableHasIsA)) {
                base = flecs_get_base_component(world, table, id, idr, 0);
            }

            if (!base) {
                /* Normal ctor */
                ecs_xtor_t ctor = ti->hooks.ctor;
                if (ctor) {
                    ctor(cmd_value, 1, ti);
                }
            } else {
                /* Override */
                ecs_copy_t copy = ti->hooks.copy_ctor;
                if (copy) {
                    copy(cmd_value, base, 1, ti);
                } else {
                    ecs_os_memcpy(cmd_value, base, size);
                }
            }
        }
    } else if (value) {
        /* If component exists and value is provided, copy */
        ecs_copy_t copy = ti->hooks.copy;
        if (copy) {
            copy(existing, value, 1, ti);
        } else {
            ecs_os_memcpy(existing, value, size);
        }
    }

    if (!cmd) {
        /* If cmd is NULL, entity was already deleted. Check if we need to
         * insert a command into the queue. */
        if (!ti->hooks.dtor) {
            /* If temporary memory does not need to be destructed, it'll get 
             * freed when the stack allocator is reset. This prevents us
             * from having to insert a command when the entity was 
             * already deleted. */
            return cmd_value;
        }
        cmd = flecs_cmd_alloc(stage);
    }

    if (!existing) {
        /* If component didn't exist yet, insert command that will create it */
        cmd->kind = cmd_kind;
        cmd->id = id;
        cmd->idr = idr;
        cmd->entity = entity;
        cmd->is._1.size = size;
        cmd->is._1.value = cmd_value;
    } else {
        /* If component already exists, still insert an Add command to ensure
         * that any preceding remove commands won't remove the component. If the
         * operation is a set, also insert a Modified command. */
        if (cmd_kind == EcsOpSet) {
            cmd->kind = EcsOpAddModified; 
        } else {
            cmd->kind = EcsOpAdd;
        }
        cmd->id = id;
        cmd->entity = entity;
    }

    return cmd_value;
error:
    return NULL;
}

void flecs_stage_merge_post_frame(
    ecs_world_t *world,
    ecs_stage_t *stage)
{
    /* Execute post frame actions */
    int32_t i, count = ecs_vec_count(&stage->post_frame_actions);
    ecs_action_elem_t *elems = ecs_vec_first(&stage->post_frame_actions);
    for (i = 0; i < count; i ++) {
        elems[i].action(world, elems[i].ctx);
    }

    ecs_vec_clear(&stage->post_frame_actions);
}

void flecs_stage_init(
    ecs_world_t *world,
    ecs_stage_t *stage)
{
    ecs_poly_assert(world, ecs_world_t);
    ecs_poly_init(stage, ecs_stage_t);

    stage->world = world;
    stage->thread_ctx = world;
    stage->auto_merge = true;
    stage->async = false;

    flecs_stack_init(&stage->defer_stack);
    flecs_stack_init(&stage->allocators.iter_stack);
    flecs_stack_init(&stage->allocators.deser_stack);
    flecs_allocator_init(&stage->allocator);
    flecs_ballocator_init_n(&stage->allocators.cmd_entry_chunk, ecs_cmd_entry_t,
        FLECS_SPARSE_PAGE_SIZE);

    ecs_allocator_t *a = &stage->allocator;
    ecs_vec_init_t(a, &stage->commands, ecs_cmd_t, 0);
    ecs_vec_init_t(a, &stage->post_frame_actions, ecs_action_elem_t, 0);
    flecs_sparse_init_t(&stage->cmd_entries, &stage->allocator,
        &stage->allocators.cmd_entry_chunk, ecs_cmd_entry_t);
}

void flecs_stage_fini(
    ecs_world_t *world,
    ecs_stage_t *stage)
{
    (void)world;
    ecs_poly_assert(world, ecs_world_t);
    ecs_poly_assert(stage, ecs_stage_t);

    /* Make sure stage has no unmerged data */
    ecs_assert(ecs_vec_count(&stage->commands) == 0, ECS_INTERNAL_ERROR, NULL);

    ecs_poly_fini(stage, ecs_stage_t);

    flecs_sparse_fini(&stage->cmd_entries);

    ecs_allocator_t *a = &stage->allocator;
    ecs_vec_fini_t(a, &stage->commands, ecs_cmd_t);
    ecs_vec_fini_t(a, &stage->post_frame_actions, ecs_action_elem_t);
    ecs_vec_fini(NULL, &stage->variables, 0);
    ecs_vec_fini(NULL, &stage->operations, 0);
    flecs_stack_fini(&stage->defer_stack);
    flecs_stack_fini(&stage->allocators.iter_stack);
    flecs_stack_fini(&stage->allocators.deser_stack);
    flecs_ballocator_fini(&stage->allocators.cmd_entry_chunk);
    flecs_allocator_fini(&stage->allocator);
}

void ecs_set_stage_count(
    ecs_world_t *world,
    int32_t stage_count)
{
    ecs_poly_assert(world, ecs_world_t);

    /* World must have at least one default stage */
    ecs_assert(stage_count >= 1 || (world->flags & EcsWorldFini), 
        ECS_INTERNAL_ERROR, NULL);

    bool auto_merge = true;
    ecs_entity_t *lookup_path = NULL;
    ecs_entity_t scope = 0;
    ecs_entity_t with = 0;
    if (world->stage_count >= 1) {
        auto_merge = world->stages[0].auto_merge;
        lookup_path = world->stages[0].lookup_path;
        scope = world->stages[0].scope;
        with = world->stages[0].with;
    }

    int32_t i, count = world->stage_count;
    if (count && count != stage_count) {
        ecs_stage_t *stages = world->stages;

        for (i = 0; i < count; i ++) {
            /* If stage contains a thread handle, ecs_set_threads was used to
             * create the stages. ecs_set_threads and ecs_set_stage_count should not
             * be mixed. */
            ecs_poly_assert(&stages[i], ecs_stage_t);
            ecs_check(stages[i].thread == 0, ECS_INVALID_OPERATION, NULL);
            flecs_stage_fini(world, &stages[i]);
        }

        ecs_os_free(world->stages);
    }

    if (stage_count) {
        world->stages = ecs_os_malloc_n(ecs_stage_t, stage_count);

        for (i = 0; i < stage_count; i ++) {
            ecs_stage_t *stage = &world->stages[i];
            flecs_stage_init(world, stage);
            stage->id = i;

            /* Set thread_ctx to stage, as this stage might be used in a
             * multithreaded context */
            stage->thread_ctx = (ecs_world_t*)stage;
            stage->thread = 0;
        }
    } else {
        /* Set to NULL to prevent double frees */
        world->stages = NULL;
    }

    /* Regardless of whether the stage was just initialized or not, when the
     * ecs_set_stage_count function is called, all stages inherit the auto_merge
     * property from the world */
    for (i = 0; i < stage_count; i ++) {
        world->stages[i].auto_merge = auto_merge;
        world->stages[i].lookup_path = lookup_path;
        world->stages[0].scope = scope;
        world->stages[0].with = with;
    }

    world->stage_count = stage_count;
error:
    return;
}

int32_t ecs_get_stage_count(
    const ecs_world_t *world)
{
    world = ecs_get_world(world);
    return world->stage_count;
}

int32_t ecs_get_stage_id(
    const ecs_world_t *world)
{
    ecs_check(world != NULL, ECS_INVALID_PARAMETER, NULL);

    if (ecs_poly_is(world, ecs_stage_t)) {
        ecs_stage_t *stage = (ecs_stage_t*)world;

        /* Index 0 is reserved for main stage */
        return stage->id;
    } else if (ecs_poly_is(world, ecs_world_t)) {
        return 0;
    } else {
        ecs_throw(ECS_INTERNAL_ERROR, NULL);
    }
error:
    return 0;
}

ecs_world_t* ecs_get_stage(
    const ecs_world_t *world,
    int32_t stage_id)
{
    ecs_poly_assert(world, ecs_world_t);
    ecs_check(world->stage_count > stage_id, ECS_INVALID_PARAMETER, NULL);
    return (ecs_world_t*)&world->stages[stage_id];
error:
    return NULL;
}

bool ecs_readonly_begin(
    ecs_world_t *world)
{
    ecs_poly_assert(world, ecs_world_t);

    flecs_process_pending_tables(world);

    ecs_dbg_3("#[bold]readonly");
    ecs_log_push_3();

    int32_t i, count = ecs_get_stage_count(world);
    for (i = 0; i < count; i ++) {
        ecs_stage_t *stage = &world->stages[i];
        stage->lookup_path = world->stages[0].lookup_path;
        ecs_assert(stage->defer == 0, ECS_INVALID_OPERATION, 
            "deferred mode cannot be enabled when entering readonly mode");
        flecs_defer_begin(world, stage);
    }

    bool is_readonly = ECS_BIT_IS_SET(world->flags, EcsWorldReadonly);

    /* From this point on, the world is "locked" for mutations, and it is only 
     * allowed to enqueue commands from stages */
    ECS_BIT_SET(world->flags, EcsWorldReadonly);

    /* If world has more than one stage, signal we might be running on multiple
     * threads. This is a stricter version of readonly mode: while some 
     * mutations like implicit component registration are still allowed in plain
     * readonly mode, no mutations are allowed when multithreaded. */
    if (world->worker_cond) {
        ECS_BIT_SET(world->flags, EcsWorldMultiThreaded);
    }

    return is_readonly;
}

void ecs_readonly_end(
    ecs_world_t *world)
{
    ecs_poly_assert(world, ecs_world_t);
    ecs_check(world->flags & EcsWorldReadonly, ECS_INVALID_OPERATION, NULL);

    /* After this it is safe again to mutate the world directly */
    ECS_BIT_CLEAR(world->flags, EcsWorldReadonly);
    ECS_BIT_CLEAR(world->flags, EcsWorldMultiThreaded);

    ecs_log_pop_3();

    flecs_stage_auto_merge(world);
error:
    return;
}

void ecs_merge(
    ecs_world_t *world)
{
    ecs_check(world != NULL, ECS_INVALID_PARAMETER, NULL);
    ecs_check(ecs_poly_is(world, ecs_world_t) || 
               ecs_poly_is(world, ecs_stage_t), ECS_INVALID_PARAMETER, NULL);
    flecs_stage_manual_merge(world);
error:
    return;
}

void ecs_set_automerge(
    ecs_world_t *world,
    bool auto_merge)
{
    /* If a world is provided, set auto_merge globally for the world. This
     * doesn't actually do anything (the main stage never merges) but it serves
     * as the default for when stages are created. */
    if (ecs_poly_is(world, ecs_world_t)) {
        world->stages[0].auto_merge = auto_merge;

        /* Propagate change to all stages */
        int i, stage_count = ecs_get_stage_count(world);
        for (i = 0; i < stage_count; i ++) {
            ecs_stage_t *stage = (ecs_stage_t*)ecs_get_stage(world, i);
            stage->auto_merge = auto_merge;
        }

    /* If a stage is provided, override the auto_merge value for the individual
     * stage. This allows an application to control per-stage which stage should
     * be automatically merged and which one shouldn't */
    } else {
        ecs_poly_assert(world, ecs_stage_t);
        ecs_stage_t *stage = (ecs_stage_t*)world;
        stage->auto_merge = auto_merge;
    }
}

bool ecs_stage_is_readonly(
    const ecs_world_t *stage)
{
    const ecs_world_t *world = ecs_get_world(stage);

    if (ecs_poly_is(stage, ecs_stage_t)) {
        if (((ecs_stage_t*)stage)->async) {
            return false;
        }
    }

    if (world->flags & EcsWorldReadonly) {
        if (ecs_poly_is(stage, ecs_world_t)) {
            return true;
        }
    } else {
        if (ecs_poly_is(stage, ecs_stage_t)) {
            return true;
        }
    }

    return false;
}

ecs_world_t* ecs_async_stage_new(
    ecs_world_t *world)
{
    ecs_stage_t *stage = ecs_os_calloc(sizeof(ecs_stage_t));
    flecs_stage_init(world, stage);

    stage->id = -1;
    stage->auto_merge = false;
    stage->async = true;

    flecs_defer_begin(world, stage);

    return (ecs_world_t*)stage;
}

void ecs_async_stage_free(
    ecs_world_t *world)
{
    ecs_poly_assert(world, ecs_stage_t);
    ecs_stage_t *stage = (ecs_stage_t*)world;
    ecs_check(stage->async == true, ECS_INVALID_PARAMETER, NULL);
    flecs_stage_fini(stage->world, stage);
    ecs_os_free(stage);
error:
    return;
}

bool ecs_stage_is_async(
    ecs_world_t *stage)
{
    if (!stage) {
        return false;
    }
    
    if (!ecs_poly_is(stage, ecs_stage_t)) {
        return false;
    }

    return ((ecs_stage_t*)stage)->async;
}

bool ecs_is_deferred(
    const ecs_world_t *world)
{
    ecs_check(world != NULL, ECS_INVALID_PARAMETER, NULL);
    const ecs_stage_t *stage = flecs_stage_from_readonly_world(world);
    return stage->defer > 0;
error:
    return false;
}

/**
 * @file datastructures/allocator.c
 * @brief Allocator for any size.
 * 
 * Allocators create a block allocator for each requested size.
 */


static
ecs_size_t flecs_allocator_size(
    ecs_size_t size)
{
    return ECS_ALIGN(size, 16);
}

static
ecs_size_t flecs_allocator_size_hash(
    ecs_size_t size)
{
    return size >> 4;
}

void flecs_allocator_init(
    ecs_allocator_t *a)
{
    flecs_ballocator_init_n(&a->chunks, ecs_block_allocator_t,
        FLECS_SPARSE_PAGE_SIZE);
    flecs_sparse_init_t(&a->sizes, NULL, &a->chunks, ecs_block_allocator_t);
}

void flecs_allocator_fini(
    ecs_allocator_t *a)
{
    int32_t i = 0, count = flecs_sparse_count(&a->sizes);
    for (i = 0; i < count; i ++) {
        ecs_block_allocator_t *ba = flecs_sparse_get_dense_t(
            &a->sizes, ecs_block_allocator_t, i);
        flecs_ballocator_fini(ba);
    }
    flecs_sparse_fini(&a->sizes);
    flecs_ballocator_fini(&a->chunks);
}

ecs_block_allocator_t* flecs_allocator_get(
    ecs_allocator_t *a, 
    ecs_size_t size)
{
    ecs_assert(size >= 0, ECS_INTERNAL_ERROR, NULL);
    if (!size) {
        return NULL;
    }

    ecs_assert(a != NULL, ECS_INTERNAL_ERROR, NULL);
    ecs_assert(size <= flecs_allocator_size(size), ECS_INTERNAL_ERROR, NULL);
    size = flecs_allocator_size(size);
    ecs_size_t hash = flecs_allocator_size_hash(size);
    ecs_block_allocator_t *result = flecs_sparse_get_any_t(&a->sizes, 
        ecs_block_allocator_t, (uint32_t)hash);

    if (!result) {
        result = flecs_sparse_ensure_fast_t(&a->sizes, 
            ecs_block_allocator_t, (uint32_t)hash);
        flecs_ballocator_init(result, size);
    }

    ecs_assert(result->data_size == size, ECS_INTERNAL_ERROR, NULL);

    return result;
}

char* flecs_strdup(
    ecs_allocator_t *a, 
    const char* str)
{
    ecs_size_t len = ecs_os_strlen(str);
    char *result = flecs_alloc_n(a, char, len + 1);
    ecs_os_memcpy(result, str, len + 1);
    return result;
}

void flecs_strfree(
    ecs_allocator_t *a, 
    char* str)
{
    ecs_size_t len = ecs_os_strlen(str);
    flecs_free_n(a, char, len + 1, str);
}

void* flecs_dup(
    ecs_allocator_t *a,
    ecs_size_t size,
    const void *src)
{
    ecs_block_allocator_t *ba = flecs_allocator_get(a, size);
    if (ba) {
        void *dst = flecs_balloc(ba);
        ecs_os_memcpy(dst, src, size);
        return dst;
    } else {
        return NULL;
    }
}

/**
 * @file datastructures/sparse.c
 * @brief Sparse set data structure.
 */


/** Compute the page index from an id by stripping the first 12 bits */
#define PAGE(index) ((int32_t)((uint32_t)index >> FLECS_SPARSE_PAGE_BITS))

/** This computes the offset of an index inside a page */
#define OFFSET(index) ((int32_t)index & (FLECS_SPARSE_PAGE_SIZE - 1))

/* Utility to get a pointer to the payload */
#define DATA(array, size, offset) (ECS_OFFSET(array, size * offset))

typedef struct ecs_page_t {
    int32_t *sparse;            /* Sparse array with indices to dense array */
    void *data;                 /* Store data in sparse array to reduce  
                                 * indirection and provide stable pointers. */
} ecs_page_t;

static
ecs_page_t* flecs_sparse_page_new(
    ecs_sparse_t *sparse,
    int32_t page_index)
{
    ecs_allocator_t *a = sparse->allocator;
    ecs_block_allocator_t *ca = sparse->page_allocator;
    int32_t count = ecs_vec_count(&sparse->pages);
    ecs_page_t *pages;

    if (count <= page_index) {
        ecs_vec_set_count_t(a, &sparse->pages, ecs_page_t, page_index + 1);
        pages = ecs_vec_first_t(&sparse->pages, ecs_page_t);
        ecs_os_memset_n(&pages[count], 0, ecs_page_t, (1 + page_index - count));
    } else {
        pages = ecs_vec_first_t(&sparse->pages, ecs_page_t);
    }

    ecs_assert(pages != NULL, ECS_INTERNAL_ERROR, NULL);

    ecs_page_t *result = &pages[page_index];
    ecs_assert(result->sparse == NULL, ECS_INTERNAL_ERROR, NULL);
    ecs_assert(result->data == NULL, ECS_INTERNAL_ERROR, NULL);

    /* Initialize sparse array with zero's, as zero is used to indicate that the
     * sparse element has not been paired with a dense element. Use zero
     * as this means we can take advantage of calloc having a possibly better 
     * performance than malloc + memset. */
    result->sparse = ca ? flecs_bcalloc(ca)
                        : ecs_os_calloc_n(int32_t, FLECS_SPARSE_PAGE_SIZE);

    /* Initialize the data array with zero's to guarantee that data is 
     * always initialized. When an entry is removed, data is reset back to
     * zero. Initialize now, as this can take advantage of calloc. */
    result->data = a ? flecs_calloc(a, sparse->size * FLECS_SPARSE_PAGE_SIZE)
                     : ecs_os_calloc(sparse->size * FLECS_SPARSE_PAGE_SIZE);

    ecs_assert(result->sparse != NULL, ECS_INTERNAL_ERROR, NULL);
    ecs_assert(result->data != NULL, ECS_INTERNAL_ERROR, NULL);

    return result;
}

static
void flecs_sparse_page_free(
    ecs_sparse_t *sparse,
    ecs_page_t *page)
{
    ecs_allocator_t *a = sparse->allocator;
    ecs_block_allocator_t *ca = sparse->page_allocator;

    if (ca) {
        flecs_bfree(ca, page->sparse);
    } else {
        ecs_os_free(page->sparse);
    }
    if (a) {
        flecs_free(a, sparse->size * FLECS_SPARSE_PAGE_SIZE, page->data);
    } else {
        ecs_os_free(page->data);
    }
}

static
ecs_page_t* flecs_sparse_get_page(
    const ecs_sparse_t *sparse,
    int32_t page_index)
{
    ecs_assert(page_index >= 0, ECS_INVALID_PARAMETER, NULL);
    if (page_index >= ecs_vec_count(&sparse->pages)) {
        return NULL;
    }
    return ecs_vec_get_t(&sparse->pages, ecs_page_t, page_index);;
}

static
ecs_page_t* flecs_sparse_get_or_create_page(
    ecs_sparse_t *sparse,
    int32_t page_index)
{
    ecs_page_t *page = flecs_sparse_get_page(sparse, page_index);
    if (page && page->sparse) {
        return page;
    }

    return flecs_sparse_page_new(sparse, page_index);
}

static
void flecs_sparse_grow_dense(
    ecs_sparse_t *sparse)
{
    ecs_vec_append_t(sparse->allocator, &sparse->dense, uint64_t);
}

static
uint64_t flecs_sparse_strip_generation(
    uint64_t *index_out)
{
    uint64_t index = *index_out;
    uint64_t gen = index & ECS_GENERATION_MASK;
    /* Make sure there's no junk in the id */
    ecs_assert(gen == (index & (0xFFFFFFFFull << 32)),
        ECS_INVALID_PARAMETER, NULL);
    *index_out -= gen;
    return gen;
}

static
void flecs_sparse_assign_index(
    ecs_page_t * page, 
    uint64_t * dense_array, 
    uint64_t index, 
    int32_t dense)
{
    /* Initialize sparse-dense pair. This assigns the dense index to the sparse
     * array, and the sparse index to the dense array .*/
    page->sparse[OFFSET(index)] = dense;
    dense_array[dense] = index;
}

static
uint64_t flecs_sparse_inc_gen(
    uint64_t index)
{
    /* When an index is deleted, its generation is increased so that we can do
     * liveliness checking while recycling ids */
    return ECS_GENERATION_INC(index);
}

static
uint64_t flecs_sparse_inc_id(
    ecs_sparse_t *sparse)
{
    /* Generate a new id. The last issued id could be stored in an external
     * variable, such as is the case with the last issued entity id, which is
     * stored on the world. */
    return ++ sparse->max_id;
}

static
uint64_t flecs_sparse_get_id(
    const ecs_sparse_t *sparse)
{
    ecs_assert(sparse != NULL, ECS_INTERNAL_ERROR, NULL);
    return sparse->max_id;
}

static
void flecs_sparse_set_id(
    ecs_sparse_t *sparse,
    uint64_t value)
{
    /* Sometimes the max id needs to be assigned directly, which typically 
     * happens when the API calls get_or_create for an id that hasn't been 
     * issued before. */
    sparse->max_id = value;
}

/* Pair dense id with new sparse id */
static
uint64_t flecs_sparse_create_id(
    ecs_sparse_t *sparse,
    int32_t dense)
{
    uint64_t index = flecs_sparse_inc_id(sparse);
    flecs_sparse_grow_dense(sparse);

    ecs_page_t *page = flecs_sparse_get_or_create_page(sparse, PAGE(index));
    ecs_assert(page->sparse[OFFSET(index)] == 0, ECS_INTERNAL_ERROR, NULL);
    
    uint64_t *dense_array = ecs_vec_first_t(&sparse->dense, uint64_t);
    flecs_sparse_assign_index(page, dense_array, index, dense);
    
    return index;
}

/* Create new id */
static
uint64_t flecs_sparse_new_index(
    ecs_sparse_t *sparse)
{
    int32_t dense_count = ecs_vec_count(&sparse->dense);
    int32_t count = sparse->count ++;

    ecs_assert(count <= dense_count, ECS_INTERNAL_ERROR, NULL);
    if (count < dense_count) {
        /* If there are unused elements in the dense array, return first */
        uint64_t *dense_array = ecs_vec_first_t(&sparse->dense, uint64_t);
        return dense_array[count];
    } else {
        return flecs_sparse_create_id(sparse, count);
    }
}

/* Get value from sparse set when it is guaranteed that the value exists. This
 * function is used when values are obtained using a dense index */
static
void* flecs_sparse_get_sparse(
    const ecs_sparse_t *sparse,
    int32_t dense,
    uint64_t index)
{
    flecs_sparse_strip_generation(&index);
    ecs_page_t *page = flecs_sparse_get_page(sparse, PAGE(index));
    if (!page || !page->sparse) {
        return NULL;
    }

    int32_t offset = OFFSET(index);
    ecs_assert(page != NULL, ECS_INTERNAL_ERROR, NULL);
    ecs_assert(dense == page->sparse[offset], ECS_INTERNAL_ERROR, NULL);
    (void)dense;

    return DATA(page->data, sparse->size, offset);
}

/* Swap dense elements. A swap occurs when an element is removed, or when a
 * removed element is recycled. */
static
void flecs_sparse_swap_dense(
    ecs_sparse_t * sparse,
    ecs_page_t * page_a,
    int32_t a,
    int32_t b)
{
    uint64_t *dense_array = ecs_vec_first_t(&sparse->dense, uint64_t);
    uint64_t index_a = dense_array[a];
    uint64_t index_b = dense_array[b];

    ecs_page_t *page_b = flecs_sparse_get_or_create_page(sparse, PAGE(index_b));
    flecs_sparse_assign_index(page_a, dense_array, index_a, b);
    flecs_sparse_assign_index(page_b, dense_array, index_b, a);
}

void flecs_sparse_init(
    ecs_sparse_t *result,
    struct ecs_allocator_t *allocator,
    ecs_block_allocator_t *page_allocator,
    ecs_size_t size)
{
    ecs_assert(result != NULL, ECS_OUT_OF_MEMORY, NULL);
    result->size = size;
    result->max_id = UINT64_MAX;
    result->allocator = allocator;
    result->page_allocator = page_allocator;

    ecs_vec_init_t(allocator, &result->pages, ecs_page_t, 0);
    ecs_vec_init_t(allocator, &result->dense, uint64_t, 1);
    result->dense.count = 1;

    /* Consume first value in dense array as 0 is used in the sparse array to
     * indicate that a sparse element hasn't been paired yet. */
    ecs_vec_first_t(&result->dense, uint64_t)[0] = 0;

    result->count = 1;
}

void flecs_sparse_clear(
    ecs_sparse_t *sparse)
{
    ecs_assert(sparse != NULL, ECS_INVALID_PARAMETER, NULL);

    int32_t i, count = ecs_vec_count(&sparse->pages);
    ecs_page_t *pages = ecs_vec_first_t(&sparse->pages, ecs_page_t);
    for (i = 0; i < count; i ++) {
        int32_t *indices = pages[i].sparse;
        if (indices) {
            ecs_os_memset_n(indices, 0, int32_t, FLECS_SPARSE_PAGE_SIZE);
        }
    }

    ecs_vec_set_count_t(sparse->allocator, &sparse->dense, uint64_t, 1);

    sparse->count = 1;
    sparse->max_id = 0;
}

void flecs_sparse_fini(
    ecs_sparse_t *sparse)
{
    ecs_assert(sparse != NULL, ECS_INTERNAL_ERROR, NULL);
    
    int32_t i, count = ecs_vec_count(&sparse->pages);
    ecs_page_t *pages = ecs_vec_first_t(&sparse->pages, ecs_page_t);
    for (i = 0; i < count; i ++) {
        flecs_sparse_page_free(sparse, &pages[i]);
    }

    ecs_vec_fini_t(sparse->allocator, &sparse->pages, ecs_page_t);
    ecs_vec_fini_t(sparse->allocator, &sparse->dense, uint64_t);
}

uint64_t flecs_sparse_new_id(
    ecs_sparse_t *sparse)
{
    ecs_assert(sparse != NULL, ECS_INVALID_PARAMETER, NULL);
    return flecs_sparse_new_index(sparse);
}

void* flecs_sparse_add(
    ecs_sparse_t *sparse,
    ecs_size_t size)
{
    ecs_assert(sparse != NULL, ECS_INVALID_PARAMETER, NULL);
    ecs_assert(!size || size == sparse->size, ECS_INVALID_PARAMETER, NULL);
    uint64_t index = flecs_sparse_new_index(sparse);
    ecs_page_t *page = flecs_sparse_get_page(sparse, PAGE(index));
    ecs_assert(page != NULL, ECS_INTERNAL_ERROR, NULL);
    return DATA(page->data, size, OFFSET(index));
}

uint64_t flecs_sparse_last_id(
    const ecs_sparse_t *sparse)
{
    ecs_assert(sparse != NULL, ECS_INTERNAL_ERROR, NULL);
    uint64_t *dense_array = ecs_vec_first_t(&sparse->dense, uint64_t);
    return dense_array[sparse->count - 1];
}

void* flecs_sparse_ensure(
    ecs_sparse_t *sparse,
    ecs_size_t size,
    uint64_t index)
{
    ecs_assert(sparse != NULL, ECS_INVALID_PARAMETER, NULL);
    ecs_assert(!size || size == sparse->size, ECS_INVALID_PARAMETER, NULL);
    ecs_assert(ecs_vec_count(&sparse->dense) > 0, ECS_INTERNAL_ERROR, NULL);
    (void)size;

    uint64_t gen = flecs_sparse_strip_generation(&index);
    ecs_page_t *page = flecs_sparse_get_or_create_page(sparse, PAGE(index));
    int32_t offset = OFFSET(index);
    int32_t dense = page->sparse[offset];

    if (dense) {
        /* Check if element is alive. If element is not alive, update indices so
         * that the first unused dense element points to the sparse element. */
        int32_t count = sparse->count;
        if (dense >= count) {
            /* If dense is not alive, swap it with the first unused element. */
            flecs_sparse_swap_dense(sparse, page, dense, count);
            dense = count;

            /* First unused element is now last used element */
            sparse->count ++;
        } else {
            /* Dense is already alive, nothing to be done */
        }

        /* Ensure provided generation matches current. Only allow mismatching
         * generations if the provided generation count is 0. This allows for
         * using the ensure function in combination with ids that have their
         * generation stripped. */
#ifdef FLECS_DEBUG
        uint64_t *dense_array = ecs_vec_first_t(&sparse->dense, uint64_t);
        ecs_assert(!gen || dense_array[dense] == (index | gen), ECS_INTERNAL_ERROR, NULL);
#endif
    } else {
        /* Element is not paired yet. Must add a new element to dense array */
        flecs_sparse_grow_dense(sparse);

        uint64_t *dense_array = ecs_vec_first_t(&sparse->dense, uint64_t);    
        int32_t dense_count = ecs_vec_count(&sparse->dense) - 1;
        int32_t count = sparse->count ++;

        /* If index is larger than max id, update max id */
        if (index >= flecs_sparse_get_id(sparse)) {
            flecs_sparse_set_id(sparse, index);
        }

        if (count < dense_count) {
            /* If there are unused elements in the list, move the first unused
             * element to the end of the list */
            uint64_t unused = dense_array[count];
            ecs_page_t *unused_page = flecs_sparse_get_or_create_page(sparse, PAGE(unused));
            flecs_sparse_assign_index(unused_page, dense_array, unused, dense_count);
        }

        flecs_sparse_assign_index(page, dense_array, index, count);
        dense_array[count] |= gen;
    }

    return DATA(page->data, sparse->size, offset);
}

void* flecs_sparse_ensure_fast(
    ecs_sparse_t *sparse,
    ecs_size_t size,
    uint64_t index_long)
{
    ecs_assert(sparse != NULL, ECS_INVALID_PARAMETER, NULL);
    ecs_assert(!size || size == sparse->size, ECS_INVALID_PARAMETER, NULL);
    ecs_assert(ecs_vec_count(&sparse->dense) > 0, ECS_INTERNAL_ERROR, NULL);
    (void)size;

    uint32_t index = (uint32_t)index_long;
    ecs_page_t *page = flecs_sparse_get_or_create_page(sparse, PAGE(index));
    int32_t offset = OFFSET(index);
    int32_t dense = page->sparse[offset];
    int32_t count = sparse->count;

    if (!dense) {
        /* Element is not paired yet. Must add a new element to dense array */
        sparse->count = count + 1;
        if (count == ecs_vec_count(&sparse->dense)) {
            flecs_sparse_grow_dense(sparse);
        }

        uint64_t *dense_array = ecs_vec_first_t(&sparse->dense, uint64_t);
        flecs_sparse_assign_index(page, dense_array, index, count);
    }

    return DATA(page->data, sparse->size, offset);
}

void flecs_sparse_remove(
    ecs_sparse_t *sparse,
    ecs_size_t size,
    uint64_t index)
{
    ecs_assert(sparse != NULL, ECS_INVALID_PARAMETER, NULL);
    ecs_assert(!size || size == sparse->size, ECS_INVALID_PARAMETER, NULL);
    (void)size;

    ecs_page_t *page = flecs_sparse_get_page(sparse, PAGE(index));
    if (!page || !page->sparse) {
        return;
    }

    uint64_t gen = flecs_sparse_strip_generation(&index);
    int32_t offset = OFFSET(index);
    int32_t dense = page->sparse[offset];

    if (dense) {
        uint64_t *dense_array = ecs_vec_first_t(&sparse->dense, uint64_t);
        uint64_t cur_gen = dense_array[dense] & ECS_GENERATION_MASK;
        if (gen != cur_gen) {
            /* Generation doesn't match which means that the provided entity is
             * already not alive. */
            return;
        }

        /* Increase generation */
        dense_array[dense] = index | flecs_sparse_inc_gen(cur_gen);
        
        int32_t count = sparse->count;
        
        if (dense == (count - 1)) {
            /* If dense is the last used element, simply decrease count */
            sparse->count --;
        } else if (dense < count) {
            /* If element is alive, move it to unused elements */
            flecs_sparse_swap_dense(sparse, page, dense, count - 1);
            sparse->count --;
        } else {
            /* Element is not alive, nothing to be done */
            return;
        }

        /* Reset memory to zero on remove */
        void *ptr = DATA(page->data, sparse->size, offset);
        ecs_os_memset(ptr, 0, size);
    } else {
        /* Element is not paired and thus not alive, nothing to be done */
        return;
    }
}

void flecs_sparse_set_generation(
    ecs_sparse_t *sparse,
    uint64_t index)
{
    ecs_assert(sparse != NULL, ECS_INVALID_PARAMETER, NULL);
    ecs_page_t *page = flecs_sparse_get_or_create_page(sparse, PAGE(index));
    
    uint64_t index_w_gen = index;
    flecs_sparse_strip_generation(&index);
    int32_t offset = OFFSET(index);
    int32_t dense = page->sparse[offset];

    if (dense) {
        /* Increase generation */
        ecs_vec_get_t(&sparse->dense, uint64_t, dense)[0] = index_w_gen;
    } else {
        /* Element is not paired and thus not alive, nothing to be done */
    }
}

void* flecs_sparse_get_dense(
    const ecs_sparse_t *sparse,
    ecs_size_t size,
    int32_t dense_index)
{
    ecs_assert(sparse != NULL, ECS_INVALID_PARAMETER, NULL);
    ecs_assert(!size || size == sparse->size, ECS_INVALID_PARAMETER, NULL);
    ecs_assert(dense_index < sparse->count, ECS_INVALID_PARAMETER, NULL);
    (void)size;

    dense_index ++;

    uint64_t *dense_array = ecs_vec_first_t(&sparse->dense, uint64_t);
    return flecs_sparse_get_sparse(sparse, dense_index, dense_array[dense_index]);
}

bool flecs_sparse_is_alive(
    const ecs_sparse_t *sparse,
    uint64_t index)
{
    ecs_page_t *page = flecs_sparse_get_page(sparse, PAGE(index));
    if (!page || !page->sparse) {
        return false;
    }

    int32_t offset = OFFSET(index);
    int32_t dense = page->sparse[offset];
    if (!dense || (dense >= sparse->count)) {
        return false;
    }

    uint64_t gen = flecs_sparse_strip_generation(&index);
    uint64_t *dense_array = ecs_vec_first_t(&sparse->dense, uint64_t);
    uint64_t cur_gen = dense_array[dense] & ECS_GENERATION_MASK;

    if (cur_gen != gen) {
        return false;
    }

    ecs_assert(dense == page->sparse[offset], ECS_INTERNAL_ERROR, NULL);
    return true;
}

void* flecs_sparse_try(
    const ecs_sparse_t *sparse,
    ecs_size_t size,
    uint64_t index)
{
    ecs_assert(sparse != NULL, ECS_INVALID_PARAMETER, NULL);
    ecs_assert(!size || size == sparse->size, ECS_INVALID_PARAMETER, NULL);
    (void)size;
    ecs_page_t *page = flecs_sparse_get_page(sparse, PAGE(index));
    if (!page || !page->sparse) {
        return NULL;
    }

    int32_t offset = OFFSET(index);
    int32_t dense = page->sparse[offset];
    if (!dense || (dense >= sparse->count)) {
        return NULL;
    }

    uint64_t gen = flecs_sparse_strip_generation(&index);
    uint64_t *dense_array = ecs_vec_first_t(&sparse->dense, uint64_t);
    uint64_t cur_gen = dense_array[dense] & ECS_GENERATION_MASK;
    if (cur_gen != gen) {
        return NULL;
    }

    ecs_assert(dense == page->sparse[offset], ECS_INTERNAL_ERROR, NULL);
    return DATA(page->data, sparse->size, offset);
}

void* flecs_sparse_get(
    const ecs_sparse_t *sparse,
    ecs_size_t size,
    uint64_t index)
{
    ecs_assert(sparse != NULL, ECS_INVALID_PARAMETER, NULL);
    ecs_assert(!size || size == sparse->size, ECS_INVALID_PARAMETER, NULL);
    (void)size;

    ecs_page_t *page = ecs_vec_get_t(&sparse->pages, ecs_page_t, PAGE(index));
    int32_t offset = OFFSET(index);
    int32_t dense = page->sparse[offset];
    ecs_assert(dense != 0, ECS_INTERNAL_ERROR, NULL);

    uint64_t gen = flecs_sparse_strip_generation(&index);
    uint64_t *dense_array = ecs_vec_first_t(&sparse->dense, uint64_t);
    uint64_t cur_gen = dense_array[dense] & ECS_GENERATION_MASK;
    (void)cur_gen; (void)gen;

    ecs_assert(cur_gen == gen, ECS_INVALID_PARAMETER, NULL);
    ecs_assert(dense == page->sparse[offset], ECS_INTERNAL_ERROR, NULL);
    ecs_assert(dense < sparse->count, ECS_INTERNAL_ERROR, NULL);
    return DATA(page->data, sparse->size, offset);
}

void* flecs_sparse_get_any(
    const ecs_sparse_t *sparse,
    ecs_size_t size,
    uint64_t index)
{
    ecs_assert(sparse != NULL, ECS_INVALID_PARAMETER, NULL);
    ecs_assert(!size || size == sparse->size, ECS_INVALID_PARAMETER, NULL);
    (void)size;
    
    flecs_sparse_strip_generation(&index);
    ecs_page_t *page = flecs_sparse_get_page(sparse, PAGE(index));
    if (!page || !page->sparse) {
        return NULL;
    }

    int32_t offset = OFFSET(index);
    int32_t dense = page->sparse[offset];
    bool in_use = dense && (dense < sparse->count);
    if (!in_use) {
        return NULL;
    }

    ecs_assert(dense == page->sparse[offset], ECS_INTERNAL_ERROR, NULL);
    return DATA(page->data, sparse->size, offset);
}

int32_t flecs_sparse_count(
    const ecs_sparse_t *sparse)
{
    if (!sparse || !sparse->count) {
        return 0;
    }

    return sparse->count - 1;
}

const uint64_t* flecs_sparse_ids(
    const ecs_sparse_t *sparse)
{
    ecs_assert(sparse != NULL, ECS_INVALID_PARAMETER, NULL);
    if (sparse->dense.array) {
        return &(ecs_vec_first_t(&sparse->dense, uint64_t)[1]);
    } else {
        return NULL;
    }
}

void ecs_sparse_init(
    ecs_sparse_t *sparse,
    ecs_size_t elem_size)
{
    flecs_sparse_init(sparse, NULL, NULL, elem_size);
}

void* ecs_sparse_add(
    ecs_sparse_t *sparse,
    ecs_size_t elem_size)
{
    return flecs_sparse_add(sparse, elem_size);
}

uint64_t ecs_sparse_last_id(
    const ecs_sparse_t *sparse)
{
    return flecs_sparse_last_id(sparse);
}

int32_t ecs_sparse_count(
    const ecs_sparse_t *sparse)
{
    return flecs_sparse_count(sparse);
}

void* ecs_sparse_get_dense(
    const ecs_sparse_t *sparse,
    ecs_size_t elem_size,
    int32_t index)
{
    return flecs_sparse_get_dense(sparse, elem_size, index);
}

void* ecs_sparse_get(
    const ecs_sparse_t *sparse,
    ecs_size_t elem_size,
    uint64_t id)
{
    return flecs_sparse_get(sparse, elem_size, id);
}

/**
 * @file datastructures/switch_list.c
 * @brief Interleaved linked list for storing mutually exclusive values.
 * 
 * Datastructure that stores N interleaved linked lists in an array. 
 * This allows for efficient storage of elements with mutually exclusive values.
 * Each linked list has a header element which points to the index in the array
 * that stores the first node of the list. Each list node points to the next
 * array element.
 *
 * The datastructure allows for efficient storage and retrieval for values with
 * mutually exclusive values, such as enumeration values. The linked list allows
 * an application to obtain all elements for a given (enumeration) value without
 * having to search.
 *
 * While the list accepts 64 bit values, it only uses the lower 32bits of the
 * value for selecting the correct linked list.
 * 
 * The switch list is used to store union relationships.
 */


#ifdef FLECS_SANITIZE
static 
void flecs_switch_verify_nodes(
    ecs_switch_header_t *hdr,
    ecs_switch_node_t *nodes)
{
    if (!hdr) {
        return;
    }

    int32_t prev = -1, elem = hdr->element, count = 0;
    while (elem != -1) {
        ecs_assert(prev == nodes[elem].prev, ECS_INTERNAL_ERROR, NULL);
        prev = elem;
        elem = nodes[elem].next;
        count ++;
    }

    ecs_assert(count == hdr->count, ECS_INTERNAL_ERROR, NULL);
}
#else
#define flecs_switch_verify_nodes(hdr, nodes)
#endif

static
ecs_switch_header_t* flecs_switch_get_header(
    const ecs_switch_t *sw,
    uint64_t value)
{
    if (value == 0) {
        return NULL;
    }
    return (ecs_switch_header_t*)ecs_map_get(&sw->hdrs, value);
}

static
ecs_switch_header_t *flecs_switch_ensure_header(
    ecs_switch_t *sw,
    uint64_t value)
{
    ecs_switch_header_t *node = flecs_switch_get_header(sw, value);
    if (!node && (value != 0)) {
        node = (ecs_switch_header_t*)ecs_map_ensure(&sw->hdrs, value);
        node->count = 0;
        node->element = -1;
    }

    return node;
}

static
void flecs_switch_remove_node(
    ecs_switch_header_t *hdr,
    ecs_switch_node_t *nodes,
    ecs_switch_node_t *node,
    int32_t element)
{
    ecs_assert(&nodes[element] == node, ECS_INTERNAL_ERROR, NULL);

    /* Update previous node/header */
    if (hdr->element == element) {
        ecs_assert(node->prev == -1, ECS_INVALID_PARAMETER, NULL);
        /* If this is the first node, update the header */
        hdr->element = node->next;
    } else {
        /* If this is not the first node, update the previous node to the 
         * removed node's next ptr */
        ecs_assert(node->prev != -1, ECS_INVALID_PARAMETER, NULL);
        ecs_switch_node_t *prev_node = &nodes[node->prev];
        prev_node->next = node->next;
    }

    /* Update next node */
    int32_t next = node->next;
    if (next != -1) {
        ecs_assert(next >= 0, ECS_INVALID_PARAMETER, NULL);
        /* If this is not the last node, update the next node to point to the
         * removed node's prev ptr */
        ecs_switch_node_t *next_node = &nodes[next];
        next_node->prev = node->prev;
    }

    /* Decrease count of current header */
    hdr->count --;
    ecs_assert(hdr->count >= 0, ECS_INTERNAL_ERROR, NULL);
}

void flecs_switch_init(
    ecs_switch_t *sw,
    ecs_allocator_t *allocator,
    int32_t elements)
{
    ecs_map_init(&sw->hdrs, allocator);
    ecs_vec_init_t(allocator, &sw->nodes, ecs_switch_node_t, elements);
    ecs_vec_init_t(allocator, &sw->values, uint64_t, elements);

    ecs_switch_node_t *nodes = ecs_vec_first(&sw->nodes);
    uint64_t *values = ecs_vec_first(&sw->values);

    int i;
    for (i = 0; i < elements; i ++) {
        nodes[i].prev = -1;
        nodes[i].next = -1;
        values[i] = 0;
    }
}

void flecs_switch_clear(
    ecs_switch_t *sw)
{
    ecs_map_clear(&sw->hdrs);
    ecs_vec_fini_t(sw->hdrs.allocator, &sw->nodes, ecs_switch_node_t);
    ecs_vec_fini_t(sw->hdrs.allocator, &sw->values, uint64_t);
}

void flecs_switch_fini(
    ecs_switch_t *sw)
{
    ecs_map_fini(&sw->hdrs);
    ecs_vec_fini_t(sw->hdrs.allocator, &sw->nodes, ecs_switch_node_t);
    ecs_vec_fini_t(sw->hdrs.allocator, &sw->values, uint64_t);
}

void flecs_switch_add(
    ecs_switch_t *sw)
{
    ecs_switch_node_t *node = ecs_vec_append_t(sw->hdrs.allocator, 
        &sw->nodes, ecs_switch_node_t);
    uint64_t *value = ecs_vec_append_t(sw->hdrs.allocator, 
        &sw->values, uint64_t);
    node->prev = -1;
    node->next = -1;
    *value = 0;
}

void flecs_switch_set_count(
    ecs_switch_t *sw,
    int32_t count)
{
    int32_t old_count = ecs_vec_count(&sw->nodes);
    if (old_count == count) {
        return;
    }

    ecs_vec_set_count_t(sw->hdrs.allocator, &sw->nodes, ecs_switch_node_t, count);
    ecs_vec_set_count_t(sw->hdrs.allocator, &sw->values, uint64_t, count);

    ecs_switch_node_t *nodes = ecs_vec_first(&sw->nodes);
    uint64_t *values = ecs_vec_first(&sw->values);

    int32_t i;
    for (i = old_count; i < count; i ++) {
        ecs_switch_node_t *node = &nodes[i];
        node->prev = -1;
        node->next = -1;
        values[i] = 0;
    }
}

int32_t flecs_switch_count(
    ecs_switch_t *sw)
{
    ecs_assert(ecs_vec_count(&sw->values) == ecs_vec_count(&sw->nodes),
        ECS_INTERNAL_ERROR, NULL);
    return ecs_vec_count(&sw->values);
}

void flecs_switch_ensure(
    ecs_switch_t *sw,
    int32_t count)
{
    int32_t old_count = ecs_vec_count(&sw->nodes);
    if (old_count >= count) {
        return;
    }

    flecs_switch_set_count(sw, count);
}

void flecs_switch_addn(
    ecs_switch_t *sw,
    int32_t count)
{
    int32_t old_count = ecs_vec_count(&sw->nodes);
    flecs_switch_set_count(sw, old_count + count);
}

void flecs_switch_set(
    ecs_switch_t *sw,
    int32_t element,
    uint64_t value)
{
    ecs_assert(sw != NULL, ECS_INVALID_PARAMETER, NULL);
    ecs_assert(element < ecs_vec_count(&sw->nodes), ECS_INVALID_PARAMETER, NULL);
    ecs_assert(element < ecs_vec_count(&sw->values), ECS_INVALID_PARAMETER, NULL);
    ecs_assert(element >= 0, ECS_INVALID_PARAMETER, NULL);

    uint64_t *values = ecs_vec_first(&sw->values);
    uint64_t cur_value = values[element];

    /* If the node is already assigned to the value, nothing to be done */
    if (cur_value == value) {
        return;
    }

    ecs_switch_node_t *nodes = ecs_vec_first(&sw->nodes);
    ecs_switch_node_t *node = &nodes[element];

    ecs_switch_header_t *dst_hdr = flecs_switch_ensure_header(sw, value);
    ecs_switch_header_t *cur_hdr = flecs_switch_get_header(sw, cur_value);

    flecs_switch_verify_nodes(cur_hdr, nodes);
    flecs_switch_verify_nodes(dst_hdr, nodes);

    /* If value is not 0, and dst_hdr is NULL, then this is not a valid value
     * for this switch */
    ecs_assert(dst_hdr != NULL || !value, ECS_INVALID_PARAMETER, NULL);

    if (cur_hdr) {
        flecs_switch_remove_node(cur_hdr, nodes, node, element);
    }

    /* Now update the node itself by adding it as the first node of dst */
    node->prev = -1;
    values[element] = value;

    if (dst_hdr) {
        node->next = dst_hdr->element;

        /* Also update the dst header */
        int32_t first = dst_hdr->element;
        if (first != -1) {
            ecs_assert(first >= 0, ECS_INTERNAL_ERROR, NULL);
            ecs_switch_node_t *first_node = &nodes[first];
            first_node->prev = element;
        }

        dst_hdr->element = element;
        dst_hdr->count ++;        
    }
}

void flecs_switch_remove(
    ecs_switch_t *sw,
    int32_t elem)
{
    ecs_assert(sw != NULL, ECS_INVALID_PARAMETER, NULL);
    ecs_assert(elem < ecs_vec_count(&sw->nodes), ECS_INVALID_PARAMETER, NULL);
    ecs_assert(elem >= 0, ECS_INVALID_PARAMETER, NULL);

    uint64_t *values = ecs_vec_first(&sw->values);
    uint64_t value = values[elem];
    ecs_switch_node_t *nodes = ecs_vec_first(&sw->nodes);
    ecs_switch_node_t *node = &nodes[elem];

    /* If node is currently assigned to a case, remove it from the list */
    if (value != 0) {
        ecs_switch_header_t *hdr = flecs_switch_get_header(sw, value);
        ecs_assert(hdr != NULL, ECS_INTERNAL_ERROR, NULL);

        flecs_switch_verify_nodes(hdr, nodes);
        flecs_switch_remove_node(hdr, nodes, node, elem);
    }

    int32_t last_elem = ecs_vec_count(&sw->nodes) - 1;
    if (last_elem != elem) {
        ecs_switch_node_t *last = ecs_vec_last_t(&sw->nodes, ecs_switch_node_t);
        int32_t next = last->next, prev = last->prev;
        if (next != -1) {
            ecs_switch_node_t *n = &nodes[next];
            n->prev = elem;
        }

        if (prev != -1) {
            ecs_switch_node_t *n = &nodes[prev];
            n->next = elem;
        } else {
            ecs_switch_header_t *hdr = flecs_switch_get_header(sw, values[last_elem]);
            if (hdr && hdr->element != -1) {
                ecs_assert(hdr->element == last_elem, 
                    ECS_INTERNAL_ERROR, NULL);
                hdr->element = elem;
            }
        }
    }

    /* Remove element from arrays */
    ecs_vec_remove_t(&sw->nodes, ecs_switch_node_t, elem);
    ecs_vec_remove_t(&sw->values, uint64_t, elem);
}

uint64_t flecs_switch_get(
    const ecs_switch_t *sw,
    int32_t element)
{
    ecs_assert(sw != NULL, ECS_INVALID_PARAMETER, NULL);
    ecs_assert(element < ecs_vec_count(&sw->nodes), ECS_INVALID_PARAMETER, NULL);
    ecs_assert(element < ecs_vec_count(&sw->values), ECS_INVALID_PARAMETER, NULL);
    ecs_assert(element >= 0, ECS_INVALID_PARAMETER, NULL);

    uint64_t *values = ecs_vec_first(&sw->values);
    return values[element];
}

ecs_vec_t* flecs_switch_values(
    const ecs_switch_t *sw)
{
    return (ecs_vec_t*)&sw->values;
}

int32_t flecs_switch_case_count(
    const ecs_switch_t *sw,
    uint64_t value)
{
    ecs_switch_header_t *hdr = flecs_switch_get_header(sw, value);
    if (!hdr) {
        return 0;
    }

    return hdr->count;
}

void flecs_switch_swap(
    ecs_switch_t *sw,
    int32_t elem_1,
    int32_t elem_2)
{
    uint64_t v1 = flecs_switch_get(sw, elem_1);
    uint64_t v2 = flecs_switch_get(sw, elem_2);

    flecs_switch_set(sw, elem_2, v1);
    flecs_switch_set(sw, elem_1, v2);
}

int32_t flecs_switch_first(
    const ecs_switch_t *sw,
    uint64_t value)
{
    ecs_assert(sw != NULL, ECS_INVALID_PARAMETER, NULL);
    
    ecs_switch_header_t *hdr = flecs_switch_get_header(sw, value);
    if (!hdr) {
        return -1;
    }

    return hdr->element;
}

int32_t flecs_switch_next(
    const ecs_switch_t *sw,
    int32_t element)
{
    ecs_assert(sw != NULL, ECS_INVALID_PARAMETER, NULL);
    ecs_assert(element < ecs_vec_count(&sw->nodes), ECS_INVALID_PARAMETER, NULL);
    ecs_assert(element >= 0, ECS_INVALID_PARAMETER, NULL);

    ecs_switch_node_t *nodes = ecs_vec_first(&sw->nodes);

    return nodes[element].next;
}


static
ecs_entity_index_page_t* flecs_entity_index_ensure_page(
    ecs_entity_index_t *index,
    uint32_t id)
{
    int32_t page_index = (int32_t)(id >> FLECS_ENTITY_PAGE_BITS);
    if (page_index >= ecs_vec_count(&index->pages)) {
        ecs_vec_set_min_count_zeromem_t(index->allocator, &index->pages, 
            ecs_entity_index_page_t*, page_index + 1);
    }

    ecs_entity_index_page_t **page_ptr = ecs_vec_get_t(&index->pages, 
        ecs_entity_index_page_t*, page_index);
    ecs_entity_index_page_t *page = *page_ptr;
    if (!page) {
        page = *page_ptr = flecs_bcalloc(&index->page_allocator);
        ecs_assert(page != NULL, ECS_OUT_OF_MEMORY, NULL);
    }

    return page;
}

void flecs_entity_index_init(
    ecs_allocator_t *allocator,
    ecs_entity_index_t *index)
{
    index->allocator = allocator;
    index->alive_count = 1;
    ecs_vec_init_t(allocator, &index->dense, uint64_t, 1);
    ecs_vec_set_count_t(allocator, &index->dense, uint64_t, 1);
    ecs_vec_init_t(allocator, &index->pages, ecs_entity_index_page_t*, 0);
    flecs_ballocator_init(&index->page_allocator, 
        ECS_SIZEOF(ecs_entity_index_page_t));
}

void flecs_entity_index_fini(
    ecs_entity_index_t *index)
{
    ecs_vec_fini_t(index->allocator, &index->dense, uint64_t);
#if defined(FLECS_SANITIZE) || defined(FLECS_USE_OS_ALLOC)
    int32_t i, count = ecs_vec_count(&index->pages);
    ecs_entity_index_page_t **pages = ecs_vec_first(&index->pages);
    for (i = 0; i < count; i ++) {
        flecs_bfree(&index->page_allocator, pages[i]);
    }
#endif
    ecs_vec_fini_t(index->allocator, &index->pages, ecs_entity_index_page_t*);
    flecs_ballocator_fini(&index->page_allocator);
}

ecs_record_t* flecs_entity_index_get_any(
    const ecs_entity_index_t *index,
    uint64_t entity)
{
    uint32_t id = (uint32_t)entity;
    int32_t page_index = (int32_t)(id >> FLECS_ENTITY_PAGE_BITS);
    ecs_entity_index_page_t *page = ecs_vec_get_t(&index->pages, 
        ecs_entity_index_page_t*, page_index)[0];
    ecs_record_t *r = &page->records[id & FLECS_ENTITY_PAGE_MASK];
    ecs_assert(r->dense != 0, ECS_INVALID_PARAMETER, NULL);
    return r;
}

ecs_record_t* flecs_entity_index_get(
    const ecs_entity_index_t *index,
    uint64_t entity)
{
    ecs_record_t *r = flecs_entity_index_get_any(index, entity);
    ecs_assert(r->dense < index->alive_count, ECS_INVALID_PARAMETER, NULL);
    ecs_assert(ecs_vec_get_t(&index->dense, uint64_t, r->dense)[0] == entity, 
        ECS_INVALID_PARAMETER, NULL);
    return r;
}

ecs_record_t* flecs_entity_index_try_get_any(
    const ecs_entity_index_t *index,
    uint64_t entity)
{
    uint32_t id = (uint32_t)entity;
    int32_t page_index = (int32_t)(id >> FLECS_ENTITY_PAGE_BITS);
    if (page_index >= ecs_vec_count(&index->pages)) {
        return NULL;
    }

    ecs_entity_index_page_t *page = ecs_vec_get_t(&index->pages, 
        ecs_entity_index_page_t*, page_index)[0];
    if (!page) {
        return NULL;
    }

    ecs_record_t *r = &page->records[id & FLECS_ENTITY_PAGE_MASK];
    if (!r->dense) {
        return NULL;
    }

    return r;
}

ecs_record_t* flecs_entity_index_try_get(
    const ecs_entity_index_t *index,
    uint64_t entity)
{
    ecs_record_t *r = flecs_entity_index_try_get_any(index, entity);
    if (r) {
        if (r->dense >= index->alive_count) {
            return NULL;
        }
        if (ecs_vec_get_t(&index->dense, uint64_t, r->dense)[0] != entity) {
            return NULL;
        }
    }
    return r;
}

ecs_record_t* flecs_entity_index_ensure(
    ecs_entity_index_t *index,
    uint64_t entity)
{
    uint32_t id = (uint32_t)entity;
    ecs_entity_index_page_t *page = flecs_entity_index_ensure_page(index, id);
    ecs_assert(page != NULL, ECS_INTERNAL_ERROR, NULL);
    ecs_record_t *r = &page->records[id & FLECS_ENTITY_PAGE_MASK];

    int32_t dense = r->dense;
    if (dense) {
        /* Entity is already alive, nothing to be done */
        if (dense < index->alive_count) {
            ecs_assert(
                ecs_vec_get_t(&index->dense, uint64_t, dense)[0] == entity, 
                ECS_INTERNAL_ERROR, NULL);
            return r;
        }
    } else {
        /* Entity doesn't have a dense index yet */
        ecs_vec_append_t(index->allocator, &index->dense, uint64_t)[0] = entity;
        r->dense = dense = ecs_vec_count(&index->dense) - 1;
        index->max_id = id > index->max_id ? id : index->max_id;
    }

    ecs_assert(dense != 0, ECS_INTERNAL_ERROR, NULL);

    /* Entity is not alive, swap with first not alive element */
    uint64_t *ids = ecs_vec_first(&index->dense);
    uint64_t e_swap = ids[index->alive_count];
    ecs_record_t *r_swap = flecs_entity_index_get_any(index, e_swap);
    ecs_assert(r_swap->dense == index->alive_count, 
        ECS_INTERNAL_ERROR, NULL);

    r_swap->dense = dense;
    r->dense = index->alive_count;
    ids[dense] = e_swap;
    ids[index->alive_count ++] = entity;

    ecs_assert(flecs_entity_index_is_alive(index, entity), 
        ECS_INTERNAL_ERROR, NULL);

    return r;
}

void flecs_entity_index_remove(
    ecs_entity_index_t *index,
    uint64_t entity)
{
    ecs_record_t *r = flecs_entity_index_try_get(index, entity);
    if (!r) {
        /* Entity is not alive or doesn't exist, nothing to be done */
        return;
    }

    int32_t dense = r->dense;
    int32_t i_swap = -- index->alive_count;
    uint64_t *e_swap_ptr = ecs_vec_get_t(&index->dense, uint64_t, i_swap);
    uint64_t e_swap = e_swap_ptr[0];
    ecs_record_t *r_swap = flecs_entity_index_get_any(index, e_swap);
    ecs_assert(r_swap->dense == i_swap, ECS_INTERNAL_ERROR, NULL);

    r_swap->dense = dense;
    r->table = NULL;
    r->idr = NULL;
    r->row = 0;
    r->dense = i_swap;
    ecs_vec_get_t(&index->dense, uint64_t, dense)[0] = e_swap;
    e_swap_ptr[0] = ECS_GENERATION_INC(entity);
    ecs_assert(!flecs_entity_index_is_alive(index, entity), 
        ECS_INTERNAL_ERROR, NULL);
}

void flecs_entity_index_set_generation(
    ecs_entity_index_t *index,
    uint64_t entity)
{
    ecs_record_t *r = flecs_entity_index_try_get_any(index, entity);
    if (r) {
        ecs_vec_get_t(&index->dense, uint64_t, r->dense)[0] = entity;
    }
}

uint64_t flecs_entity_index_get_generation(
    const ecs_entity_index_t *index,
    uint64_t entity)
{
    ecs_record_t *r = flecs_entity_index_try_get_any(index, entity);
    if (r) {
        return ecs_vec_get_t(&index->dense, uint64_t, r->dense)[0];
    } else {
        return 0;
    }
}

bool flecs_entity_index_is_alive(
    const ecs_entity_index_t *index,
    uint64_t entity)
{
    return flecs_entity_index_try_get(index, entity) != NULL;
}

bool flecs_entity_index_is_valid(
    const ecs_entity_index_t *index,
    uint64_t entity)
{
    uint32_t id = (uint32_t)entity;    
    ecs_record_t *r = flecs_entity_index_try_get_any(index, id);
    if (!r || !r->dense) {
        /* Doesn't exist yet, so is valid */
        return true;
    }

    /* If the id exists, it must be alive */
    return r->dense < index->alive_count;
}

bool flecs_entity_index_exists(
    const ecs_entity_index_t *index,
    uint64_t entity)
{
    return flecs_entity_index_try_get_any(index, entity) != NULL;
}

uint64_t flecs_entity_index_new_id(
    ecs_entity_index_t *index)
{
    if (index->alive_count != ecs_vec_count(&index->dense)) {
        /* Recycle id */
        return ecs_vec_get_t(&index->dense, uint64_t, index->alive_count ++)[0];
    }

    /* Create new id */
    uint32_t id = (uint32_t)++ index->max_id;
    ecs_vec_append_t(index->allocator, &index->dense, uint64_t)[0] = id;

    ecs_entity_index_page_t *page = flecs_entity_index_ensure_page(index, id);
    ecs_assert(page != NULL, ECS_INTERNAL_ERROR, NULL);
    ecs_record_t *r = &page->records[id & FLECS_ENTITY_PAGE_MASK];
    r->dense = index->alive_count ++;
    ecs_assert(index->alive_count == ecs_vec_count(&index->dense), 
        ECS_INTERNAL_ERROR, NULL);

    return id;
}

uint64_t* flecs_entity_index_new_ids(
    ecs_entity_index_t *index,
    int32_t count)
{
    int32_t alive_count = index->alive_count;
    int32_t new_count = alive_count + count;
    int32_t dense_count = ecs_vec_count(&index->dense);

    if (new_count < dense_count) {
        /* Recycle ids */
        index->alive_count = new_count;
        return ecs_vec_get_t(&index->dense, uint64_t, alive_count);
    }

    /* Allocate new ids */
    ecs_vec_set_count_t(index->allocator, &index->dense, uint64_t, new_count);
    int32_t i, to_add = new_count - dense_count;
    for (i = 0; i < to_add; i ++) {
        uint32_t id = (uint32_t)++ index->max_id;
        int32_t dense = dense_count + i;
        ecs_vec_get_t(&index->dense, uint64_t, dense)[0] = id;
        ecs_entity_index_page_t *page = flecs_entity_index_ensure_page(index, id);
        ecs_assert(page != NULL, ECS_INTERNAL_ERROR, NULL);
        ecs_record_t *r = &page->records[id & FLECS_ENTITY_PAGE_MASK];
        r->dense = dense;
    }

    index->alive_count = new_count;
    return ecs_vec_get_t(&index->dense, uint64_t, alive_count);
}

void flecs_entity_index_set_size(
    ecs_entity_index_t *index,
    int32_t size)
{
    ecs_vec_set_size_t(index->allocator, &index->dense, uint64_t, size);
}

int32_t flecs_entity_index_count(
    const ecs_entity_index_t *index)
{
    return index->alive_count - 1;
}

int32_t flecs_entity_index_size(
    const ecs_entity_index_t *index)
{
    return ecs_vec_count(&index->dense) - 1;
}

int32_t flecs_entity_index_not_alive_count(
    const ecs_entity_index_t *index)
{
    return ecs_vec_count(&index->dense) - index->alive_count;
}

void flecs_entity_index_clear(
    ecs_entity_index_t *index)
{
    int32_t i, count = ecs_vec_count(&index->pages);
    ecs_entity_index_page_t **pages = ecs_vec_first_t(&index->pages, 
        ecs_entity_index_page_t*);
    for (i = 0; i < count; i ++) {
        ecs_entity_index_page_t *page = pages[i];
        if (page) {
            ecs_os_zeromem(page);
        }
    }

    ecs_vec_set_count_t(index->allocator, &index->dense, uint64_t, 1);

    index->alive_count = 1;
    index->max_id = 0;
}

const uint64_t* flecs_entity_index_ids(
    const ecs_entity_index_t *index)
{
    return ecs_vec_get_t(&index->dense, uint64_t, 1);
}

static
void flecs_entity_index_copy_intern(
    ecs_entity_index_t * dst,
    const ecs_entity_index_t * src)
{
    flecs_entity_index_set_size(dst, flecs_entity_index_size(src));
    const uint64_t *ids = flecs_entity_index_ids(src);
    
    int32_t i, count = src->alive_count;
    for (i = 0; i < count - 1; i ++) {
        uint64_t id = ids[i];
        ecs_record_t *src_ptr = flecs_entity_index_get(src, id);
        ecs_record_t *dst_ptr = flecs_entity_index_ensure(dst, id);
        flecs_entity_index_set_generation(dst, id);
        ecs_os_memcpy_t(dst_ptr, src_ptr, ecs_record_t);
    }

    dst->max_id = src->max_id;

    ecs_assert(src->alive_count == dst->alive_count, ECS_INTERNAL_ERROR, NULL);
}

void flecs_entity_index_copy(
    ecs_entity_index_t *dst,
    const ecs_entity_index_t *src)
{
    if (!src) {
        return;
    }

    flecs_entity_index_init(src->allocator, dst);
    flecs_entity_index_copy_intern(dst, src);
}

void flecs_entity_index_restore(
    ecs_entity_index_t *dst,
    const ecs_entity_index_t *src)
{
    if (!src) {
        return;
    }

    flecs_entity_index_clear(dst);
    flecs_entity_index_copy_intern(dst, src);
}

/**
 * @file datastructures/name_index.c
 * @brief Data structure for resolving 64bit keys by string (name).
 */


static
uint64_t flecs_name_index_hash(
    const void *ptr)
{
    const ecs_hashed_string_t *str = ptr;
    ecs_assert(str->hash != 0, ECS_INTERNAL_ERROR, NULL);
    return str->hash;
}

static
int flecs_name_index_compare(
    const void *ptr1, 
    const void *ptr2)
{
    const ecs_hashed_string_t *str1 = ptr1;
    const ecs_hashed_string_t *str2 = ptr2;
    ecs_size_t len1 = str1->length;
    ecs_size_t len2 = str2->length;
    if (len1 != len2) {
        return (len1 > len2) - (len1 < len2);
    }

    return ecs_os_memcmp(str1->value, str2->value, len1);
}

void flecs_name_index_init(
    ecs_hashmap_t *hm,
    ecs_allocator_t *allocator) 
{
    _flecs_hashmap_init(hm, 
        ECS_SIZEOF(ecs_hashed_string_t), ECS_SIZEOF(uint64_t), 
        flecs_name_index_hash, 
        flecs_name_index_compare,
        allocator);
}

void flecs_name_index_init_if(
    ecs_hashmap_t *hm,
    ecs_allocator_t *allocator) 
{
    if (!hm->compare) {
        flecs_name_index_init(hm, allocator);
    }
}

bool flecs_name_index_is_init(
    const ecs_hashmap_t *hm)
{
    return hm->compare != NULL;
}

ecs_hashmap_t* flecs_name_index_new(
    ecs_world_t *world,
    ecs_allocator_t *allocator) 
{
    ecs_hashmap_t *result = flecs_bcalloc(&world->allocators.hashmap);
    flecs_name_index_init(result, allocator);
    result->hashmap_allocator = &world->allocators.hashmap;
    return result;
}

void flecs_name_index_fini(
    ecs_hashmap_t *map)
{
    flecs_hashmap_fini(map);
}

void flecs_name_index_free(
    ecs_hashmap_t *map)
{
    if (map) {
        flecs_name_index_fini(map);
        flecs_bfree(map->hashmap_allocator, map);
    }
}

ecs_hashmap_t* flecs_name_index_copy(
    ecs_hashmap_t *map)
{
    ecs_hashmap_t *result = flecs_bcalloc(map->hashmap_allocator);
    result->hashmap_allocator = map->hashmap_allocator;
    flecs_hashmap_copy(result, map);
    return result;
}

ecs_hashed_string_t flecs_get_hashed_string(
    const char *name,
    ecs_size_t length,
    uint64_t hash)
{
    if (!length) {
        length = ecs_os_strlen(name);
    } else {
        ecs_assert(length == ecs_os_strlen(name), ECS_INTERNAL_ERROR, NULL);
    }

    if (!hash) {
        hash = flecs_hash(name, length);
    } else {
        ecs_assert(hash == flecs_hash(name, length), ECS_INTERNAL_ERROR, NULL);
    }

    return (ecs_hashed_string_t) {
        .value = (char*)name,
        .length = length,
        .hash = hash
    };
}

const uint64_t* flecs_name_index_find_ptr(
    const ecs_hashmap_t *map,
    const char *name,
    ecs_size_t length,
    uint64_t hash)
{
    ecs_hashed_string_t hs = flecs_get_hashed_string(name, length, hash);
    ecs_hm_bucket_t *b = flecs_hashmap_get_bucket(map, hs.hash);
    if (!b) {
        return NULL;
    }

    ecs_hashed_string_t *keys = ecs_vec_first(&b->keys);
    int32_t i, count = ecs_vec_count(&b->keys);

    for (i = 0; i < count; i ++) {
        ecs_hashed_string_t *key = &keys[i];
        ecs_assert(key->hash == hs.hash, ECS_INTERNAL_ERROR, NULL);

        if (hs.length != key->length) {
            continue;
        }

        if (!ecs_os_strcmp(name, key->value)) {
            uint64_t *e = ecs_vec_get_t(&b->values, uint64_t, i);
            ecs_assert(e != NULL, ECS_INTERNAL_ERROR, NULL);
            return e;
        }
    }

    return NULL;
}

uint64_t flecs_name_index_find(
    const ecs_hashmap_t *map,
    const char *name,
    ecs_size_t length,
    uint64_t hash)
{
    const uint64_t *id = flecs_name_index_find_ptr(map, name, length, hash);
    if (id) {
        return id[0];
    }
    return 0;
}

void flecs_name_index_remove(
    ecs_hashmap_t *map,
    uint64_t e,
    uint64_t hash)
{
    ecs_hm_bucket_t *b = flecs_hashmap_get_bucket(map, hash);
    if (!b) {
        return;
    }

    uint64_t *ids = ecs_vec_first(&b->values);
    int32_t i, count = ecs_vec_count(&b->values);
    for (i = 0; i < count; i ++) {
        if (ids[i] == e) {
            flecs_hm_bucket_remove(map, b, hash, i);
            break;
        }
    }
}

void flecs_name_index_update_name(
    ecs_hashmap_t *map,
    uint64_t e,
    uint64_t hash,
    const char *name)
{
    ecs_hm_bucket_t *b = flecs_hashmap_get_bucket(map, hash);
    if (!b) {
        return;
    }

    uint64_t *ids = ecs_vec_first(&b->values);
    int32_t i, count = ecs_vec_count(&b->values);
    for (i = 0; i < count; i ++) {
        if (ids[i] == e) {
            ecs_hashed_string_t *key = ecs_vec_get_t(
                &b->keys, ecs_hashed_string_t, i);
            key->value = (char*)name;
            ecs_assert(ecs_os_strlen(name) == key->length,
                ECS_INTERNAL_ERROR, NULL);
            ecs_assert(flecs_hash(name, key->length) == key->hash,
                ECS_INTERNAL_ERROR, NULL);
            return;
        }
    }

    /* Record must already have been in the index */
    ecs_abort(ECS_INTERNAL_ERROR, NULL);
}

void flecs_name_index_ensure(
    ecs_hashmap_t *map,
    uint64_t id,
    const char *name,
    ecs_size_t length,
    uint64_t hash)
{
    ecs_check(name != NULL, ECS_INVALID_PARAMETER, NULL);

    ecs_hashed_string_t key = flecs_get_hashed_string(name, length, hash);
    
    uint64_t existing = flecs_name_index_find(
        map, name, key.length, key.hash);
    if (existing) {
        if (existing != id) {
            ecs_abort(ECS_ALREADY_DEFINED, 
                "conflicting id registered with name '%s'", name);
        }
    }

    flecs_hashmap_result_t hmr = flecs_hashmap_ensure(
        map, &key, uint64_t);
    *((uint64_t*)hmr.value) = id;
error:
    return;
}

// This is free and unencumbered software released into the public domain under The Unlicense (http://unlicense.org/)
// main repo: https://github.com/wangyi-fudan/wyhash
// author: 王一 Wang Yi
// contributors: Reini Urban, Dietrich Epp, Joshua Haberman, Tommy Ettinger, 
//               Daniel Lemire, Otmar Ertl, cocowalla, leo-yuriev, 
//               Diego Barrios Romero, paulie-g, dumblob, Yann Collet, ivte-ms, 
//               hyb, James Z.M. Gao, easyaspi314 (Devin), TheOneric

/* quick example:
   string s="fjsakfdsjkf";
   uint64_t hash=wyhash(s.c_str(), s.size(), 0, _wyp);
*/


#ifndef WYHASH_CONDOM
//protections that produce different results:
//1: normal valid behavior
//2: extra protection against entropy loss (probability=2^-63), aka. "blind multiplication"
#define WYHASH_CONDOM 1
#endif

#ifndef WYHASH_32BIT_MUM
//0: normal version, slow on 32 bit systems
//1: faster on 32 bit systems but produces different results, incompatible with wy2u0k function
#define WYHASH_32BIT_MUM 0  
#endif

//includes
#include <stdint.h>
#include <string.h>
#if defined(_MSC_VER) && defined(_M_X64)
  #include <intrin.h>
  #pragma intrinsic(_umul128)
#endif

//likely and unlikely macros
#if defined(__GNUC__) || defined(__INTEL_COMPILER) || defined(__clang__)
  #define _likely_(x)  __builtin_expect(x,1)
  #define _unlikely_(x)  __builtin_expect(x,0)
#else
  #define _likely_(x) (x)
  #define _unlikely_(x) (x)
#endif

//128bit multiply function
static inline void _wymum(uint64_t *A, uint64_t *B){
#if(WYHASH_32BIT_MUM)
  uint64_t hh=(*A>>32)*(*B>>32), hl=(*A>>32)*(uint32_t)*B, lh=(uint32_t)*A*(*B>>32), ll=(uint64_t)(uint32_t)*A*(uint32_t)*B;
  #if(WYHASH_CONDOM>1)
  *A^=_wyrot(hl)^hh; *B^=_wyrot(lh)^ll;
  #else
  *A=_wyrot(hl)^hh; *B=_wyrot(lh)^ll;
  #endif
#elif defined(__SIZEOF_INT128__)
  __uint128_t r=*A; r*=*B; 
  #if(WYHASH_CONDOM>1)
  *A^=(uint64_t)r; *B^=(uint64_t)(r>>64);
  #else
  *A=(uint64_t)r; *B=(uint64_t)(r>>64);
  #endif
#elif defined(_MSC_VER) && defined(_M_X64)
  #if(WYHASH_CONDOM>1)
  uint64_t  a,  b;
  a=_umul128(*A,*B,&b);
  *A^=a;  *B^=b;
  #else
  *A=_umul128(*A,*B,B);
  #endif
#else
  uint64_t ha=*A>>32, hb=*B>>32, la=(uint32_t)*A, lb=(uint32_t)*B, hi, lo;
  uint64_t rh=ha*hb, rm0=ha*lb, rm1=hb*la, rl=la*lb, t=rl+(rm0<<32), c=t<rl;
  lo=t+(rm1<<32); c+=lo<t; hi=rh+(rm0>>32)+(rm1>>32)+c;
  #if(WYHASH_CONDOM>1)
  *A^=lo;  *B^=hi;
  #else
  *A=lo;  *B=hi;
  #endif
#endif
}

//multiply and xor mix function, aka MUM
static inline uint64_t _wymix(uint64_t A, uint64_t B){ _wymum(&A,&B); return A^B; }

//endian macros
#ifndef WYHASH_LITTLE_ENDIAN
  #if defined(_WIN32) || defined(__LITTLE_ENDIAN__) || (defined(__BYTE_ORDER__) && __BYTE_ORDER__ == __ORDER_LITTLE_ENDIAN__)
    #define WYHASH_LITTLE_ENDIAN 1
  #elif defined(__BIG_ENDIAN__) || (defined(__BYTE_ORDER__) && __BYTE_ORDER__ == __ORDER_BIG_ENDIAN__)
    #define WYHASH_LITTLE_ENDIAN 0
  #else
    #warning could not determine endianness! Falling back to little endian.
    #define WYHASH_LITTLE_ENDIAN 1
  #endif
#endif

//read functions
#if (WYHASH_LITTLE_ENDIAN)
static inline uint64_t _wyr8(const uint8_t *p) { uint64_t v; memcpy(&v, p, 8); return v;}
static inline uint64_t _wyr4(const uint8_t *p) { uint32_t v; memcpy(&v, p, 4); return v;}
#elif defined(__GNUC__) || defined(__INTEL_COMPILER) || defined(__clang__)
static inline uint64_t _wyr8(const uint8_t *p) { uint64_t v; memcpy(&v, p, 8); return __builtin_bswap64(v);}
static inline uint64_t _wyr4(const uint8_t *p) { uint32_t v; memcpy(&v, p, 4); return __builtin_bswap32(v);}
#elif defined(_MSC_VER)
static inline uint64_t _wyr8(const uint8_t *p) { uint64_t v; memcpy(&v, p, 8); return _byteswap_uint64(v);}
static inline uint64_t _wyr4(const uint8_t *p) { uint32_t v; memcpy(&v, p, 4); return _byteswap_ulong(v);}
#else
static inline uint64_t _wyr8(const uint8_t *p) {
  uint64_t v; memcpy(&v, p, 8);
  return (((v >> 56) & 0xff)| ((v >> 40) & 0xff00)| ((v >> 24) & 0xff0000)| ((v >>  8) & 0xff000000)| ((v <<  8) & 0xff00000000)| ((v << 24) & 0xff0000000000)| ((v << 40) & 0xff000000000000)| ((v << 56) & 0xff00000000000000));
}
static inline uint64_t _wyr4(const uint8_t *p) {
  uint32_t v; memcpy(&v, p, 4);
  return (((v >> 24) & 0xff)| ((v >>  8) & 0xff00)| ((v <<  8) & 0xff0000)| ((v << 24) & 0xff000000));
}
#endif
static inline uint64_t _wyr3(const uint8_t *p, size_t k) { return (((uint64_t)p[0])<<16)|(((uint64_t)p[k>>1])<<8)|p[k-1];}

//wyhash main function
static inline uint64_t wyhash(const void *key, size_t len, uint64_t seed, const uint64_t *secret){
  const uint8_t *p=(const uint8_t *)key; seed^=_wymix(seed^secret[0],secret[1]);	uint64_t	a,	b;
  if(_likely_(len<=16)){
    if(_likely_(len>=4)){ a=(_wyr4(p)<<32)|_wyr4(p+((len>>3)<<2)); b=(_wyr4(p+len-4)<<32)|_wyr4(p+len-4-((len>>3)<<2)); }
    else if(_likely_(len>0)){ a=_wyr3(p,len); b=0;}
    else a=b=0;
  }
  else{
    size_t i=len; 
    if(_unlikely_(i>48)){
      uint64_t see1=seed, see2=seed;
      do{
        seed=_wymix(_wyr8(p)^secret[1],_wyr8(p+8)^seed);
        see1=_wymix(_wyr8(p+16)^secret[2],_wyr8(p+24)^see1);
        see2=_wymix(_wyr8(p+32)^secret[3],_wyr8(p+40)^see2);
        p+=48; i-=48;
      }while(_likely_(i>48));
      seed^=see1^see2;
    }
    while(_unlikely_(i>16)){  seed=_wymix(_wyr8(p)^secret[1],_wyr8(p+8)^seed);  i-=16; p+=16;  }
    a=_wyr8(p+i-16);  b=_wyr8(p+i-8);
  }
  a^=secret[1]; b^=seed;  _wymum(&a,&b);
  return  _wymix(a^secret[0]^len,b^secret[1]);
}

//the default secret parameters
static const uint64_t _wyp[4] = {0xa0761d6478bd642full, 0xe7037ed1a0b428dbull, 0x8ebc6af09c88c6e3ull, 0x589965cc75374cc3ull};

uint64_t flecs_hash(
    const void *data,
    ecs_size_t length)
{
    return wyhash(data, flecs_ito(size_t, length), 0, _wyp);
}

/**
 * @file datastructures/qsort.c
 * @brief Quicksort implementation.
 */


void ecs_qsort(
    void *base, 
    ecs_size_t nitems, 
    ecs_size_t size, 
    int (*compar)(const void *, const void*))
{
    void *tmp = ecs_os_alloca(size); /* For swap */

    #define LESS(i, j) \
        compar(ECS_ELEM(base, size, i), ECS_ELEM(base, size, j)) < 0

    #define SWAP(i, j) \
        ecs_os_memcpy(tmp, ECS_ELEM(base, size, i), size),\
        ecs_os_memcpy(ECS_ELEM(base, size, i), ECS_ELEM(base, size, j), size),\
        ecs_os_memcpy(ECS_ELEM(base, size, j), tmp, size)

    QSORT(nitems, LESS, SWAP);
}

/**
 * @file datastructures/bitset.c
 * @brief Bitset data structure.
 * 
 * Simple bitset implementation. The bitset allows for storage of arbitrary
 * numbers of bits.
 */


static
void ensure(
    ecs_bitset_t *bs,
    ecs_size_t size)
{
    if (!bs->size) {
        int32_t new_size = ((size - 1) / 64 + 1) * ECS_SIZEOF(uint64_t);
        bs->size = ((size - 1) / 64 + 1) * 64;
        bs->data = ecs_os_calloc(new_size);
    } else if (size > bs->size) {
        int32_t prev_size = ((bs->size - 1) / 64 + 1) * ECS_SIZEOF(uint64_t);
        bs->size = ((size - 1) / 64 + 1) * 64;
        int32_t new_size = ((size - 1) / 64 + 1) * ECS_SIZEOF(uint64_t);
        bs->data = ecs_os_realloc(bs->data, new_size);
        ecs_os_memset(ECS_OFFSET(bs->data, prev_size), 0, new_size - prev_size);
    }
}

void flecs_bitset_init(
    ecs_bitset_t* bs)
{
    bs->size = 0;
    bs->count = 0;
    bs->data = NULL;
}

void flecs_bitset_ensure(
    ecs_bitset_t *bs,
    int32_t count)
{
    if (count > bs->count) {
        bs->count = count;
        ensure(bs, count);
    }
}

void flecs_bitset_fini(
    ecs_bitset_t *bs)
{
    ecs_os_free(bs->data);
    bs->data = NULL;
    bs->count = 0;
}

void flecs_bitset_addn(
    ecs_bitset_t *bs,
    int32_t count)
{
    int32_t elem = bs->count += count;
    ensure(bs, elem);
}

void flecs_bitset_set(
    ecs_bitset_t *bs,
    int32_t elem,
    bool value)
{
    ecs_check(elem < bs->count, ECS_INVALID_PARAMETER, NULL);
    uint32_t hi = ((uint32_t)elem) >> 6;
    uint32_t lo = ((uint32_t)elem) & 0x3F;
    uint64_t v = bs->data[hi];
    bs->data[hi] = (v & ~((uint64_t)1 << lo)) | ((uint64_t)value << lo);
error:
    return;
}

bool flecs_bitset_get(
    const ecs_bitset_t *bs,
    int32_t elem)
{
    ecs_check(elem < bs->count, ECS_INVALID_PARAMETER, NULL);
    return !!(bs->data[elem >> 6] & ((uint64_t)1 << ((uint64_t)elem & 0x3F)));
error:
    return false;
}

int32_t flecs_bitset_count(
    const ecs_bitset_t *bs)
{
    return bs->count;
}

void flecs_bitset_remove(
    ecs_bitset_t *bs,
    int32_t elem)
{
    ecs_check(elem < bs->count, ECS_INVALID_PARAMETER, NULL);
    int32_t last = bs->count - 1;
    bool last_value = flecs_bitset_get(bs, last);
    flecs_bitset_set(bs, elem, last_value);
    flecs_bitset_set(bs, last, 0);
    bs->count --;
error:
    return;
}

void flecs_bitset_swap(
    ecs_bitset_t *bs,
    int32_t elem_a,
    int32_t elem_b)
{
    ecs_check(elem_a < bs->count, ECS_INVALID_PARAMETER, NULL);
    ecs_check(elem_b < bs->count, ECS_INVALID_PARAMETER, NULL);

    bool a = flecs_bitset_get(bs, elem_a);
    bool b = flecs_bitset_get(bs, elem_b);
    flecs_bitset_set(bs, elem_a, b);
    flecs_bitset_set(bs, elem_b, a);
error:
    return;
}

/**
 * @file datastructures/strbuf.c
 * @brief Utility for constructing strings.
 *
 * A buffer builds up a list of elements which individually can be up to N bytes
 * large. While appending, data is added to these elements. More elements are
 * added on the fly when needed. When an application calls ecs_strbuf_get, all
 * elements are combined in one string and the element administration is freed.
 *
 * This approach prevents reallocs of large blocks of memory, and therefore
 * copying large blocks of memory when appending to a large buffer. A buffer
 * preallocates some memory for the element overhead so that for small strings
 * there is hardly any overhead, while for large strings the overhead is offset
 * by the reduced time spent on copying memory.
 * 
 * The functionality provided by strbuf is similar to std::stringstream.
 */

#include <math.h>

/**
 *  stm32tpl --  STM32 C++ Template Peripheral Library
 *  Visit https://github.com/antongus/stm32tpl for new versions
 *
 *  Copyright (c) 2011-2020 Anton B. Gusev aka AHTOXA
 */

#define MAX_PRECISION	(10)
#define EXP_THRESHOLD   (3)
#define INT64_MAX_F ((double)INT64_MAX)

static const double rounders[MAX_PRECISION + 1] =
{
	0.5,				// 0
	0.05,				// 1
	0.005,				// 2
	0.0005,				// 3
	0.00005,			// 4
	0.000005,			// 5
	0.0000005,			// 6
	0.00000005,			// 7
	0.000000005,		// 8
	0.0000000005,		// 9
	0.00000000005		// 10
};

static
char* flecs_strbuf_itoa(
    char *buf,
    int64_t v)
{
    char *ptr = buf;
    char * p1;
	char c;

	if (!v) {
		*ptr++ = '0';
    } else {
        if (v < 0) {
            ptr[0] = '-';
            ptr ++;
            v *= -1;
        }

		char *p = ptr;
		while (v) {
            int64_t vdiv = v / 10;
            int64_t vmod = v - (vdiv * 10);
			p[0] = (char)('0' + vmod);
            p ++;
			v = vdiv;
		}

		p1 = p;

		while (p > ptr) {
			c = *--p;
			*p = *ptr;
			*ptr++ = c;
		}
		ptr = p1;
	}
    return ptr;
}

static
int flecs_strbuf_ftoa(
    ecs_strbuf_t *out, 
    double f, 
    int precision,
    char nan_delim)
{
    char buf[64];
	char * ptr = buf;
	char c;
	int64_t intPart;
    int64_t exp = 0;

    if (isnan(f)) {
        if (nan_delim) {
            ecs_strbuf_appendch(out, nan_delim);
            ecs_strbuf_appendlit(out, "NaN");
            return ecs_strbuf_appendch(out, nan_delim);
        } else {
            return ecs_strbuf_appendlit(out, "NaN");
        }
    }
    if (isinf(f)) {
        if (nan_delim) {
            ecs_strbuf_appendch(out, nan_delim);
            ecs_strbuf_appendlit(out, "Inf");
            return ecs_strbuf_appendch(out, nan_delim);
        } else {
            return ecs_strbuf_appendlit(out, "Inf");
        }
    }

	if (precision > MAX_PRECISION) {
		precision = MAX_PRECISION;
    }

	if (f < 0) {
		f = -f;
		*ptr++ = '-';
	}

	if (precision < 0) {
		if (f < 1.0) precision = 6;
		else if (f < 10.0) precision = 5;
		else if (f < 100.0) precision = 4;
		else if (f < 1000.0) precision = 3;
		else if (f < 10000.0) precision = 2;
		else if (f < 100000.0) precision = 1;
		else precision = 0;
	}

	if (precision) {
		f += rounders[precision];
    }

    /* Make sure that number can be represented as 64bit int, increase exp */
    while (f > INT64_MAX_F) {
        f /= 1000 * 1000 * 1000;
        exp += 9;
    }

	intPart = (int64_t)f;
	f -= (double)intPart;

    ptr = flecs_strbuf_itoa(ptr, intPart);

	if (precision) {
		*ptr++ = '.';
		while (precision--) {
			f *= 10.0;
			c = (char)f;
			*ptr++ = (char)('0' + c);
			f -= c;
		}
	}
	*ptr = 0;

    /* Remove trailing 0s */
    while ((&ptr[-1] != buf) && (ptr[-1] == '0')) {
        ptr[-1] = '\0';
        ptr --;
    }
    if (ptr != buf && ptr[-1] == '.') {
        ptr[-1] = '\0';
        ptr --;
    }

    /* If 0s before . exceed threshold, convert to exponent to save space 
     * without losing precision. */
    char *cur = ptr;
    while ((&cur[-1] != buf) && (cur[-1] == '0')) {
        cur --;
    }

    if (exp || ((ptr - cur) > EXP_THRESHOLD)) {
        cur[0] = '\0';
        exp += (ptr - cur);
        ptr = cur;
    }

    if (exp) {
        char *p1 = &buf[1];
        if (nan_delim) {
            ecs_os_memmove(buf + 1, buf, 1 + (ptr - buf));
            buf[0] = nan_delim;
            p1 ++;
        }

        /* Make sure that exp starts after first character */
        c = p1[0];

        if (c) {
            p1[0] = '.';
            do {
                char t = (++p1)[0];
                if (t == '.') {
                    exp ++;
                    p1 --;
                    break;
                }
                p1[0] = c;
                c = t;
                exp ++;
            } while (c);
            ptr = p1 + 1;
        } else {
            ptr = p1;
        }


        ptr[0] = 'e';
        ptr = flecs_strbuf_itoa(ptr + 1, exp);

        if (nan_delim) {
            ptr[0] = nan_delim;
            ptr ++;
        }

        ptr[0] = '\0';
    }
    
    return ecs_strbuf_appendstrn(out, buf, (int32_t)(ptr - buf));
}

/* Add an extra element to the buffer */
static
void flecs_strbuf_grow(
    ecs_strbuf_t *b)
{
    /* Allocate new element */
    ecs_strbuf_element_embedded *e = ecs_os_malloc_t(ecs_strbuf_element_embedded);
    b->size += b->current->pos;
    b->current->next = (ecs_strbuf_element*)e;
    b->current = (ecs_strbuf_element*)e;
    b->elementCount ++;
    e->super.buffer_embedded = true;
    e->super.buf = e->buf;
    e->super.pos = 0;
    e->super.next = NULL;
}

/* Add an extra dynamic element */
static
void flecs_strbuf_grow_str(
    ecs_strbuf_t *b,
    char *str,
    char *alloc_str,
    int32_t size)
{
    /* Allocate new element */
    ecs_strbuf_element_str *e = ecs_os_malloc_t(ecs_strbuf_element_str);
    b->size += b->current->pos;
    b->current->next = (ecs_strbuf_element*)e;
    b->current = (ecs_strbuf_element*)e;
    b->elementCount ++;
    e->super.buffer_embedded = false;
    e->super.pos = size ? size : (int32_t)ecs_os_strlen(str);
    e->super.next = NULL;
    e->super.buf = str;
    e->alloc_str = alloc_str;
}

static
char* flecs_strbuf_ptr(
    ecs_strbuf_t *b)
{
    if (b->buf) {
        return &b->buf[b->current->pos];
    } else {
        return &b->current->buf[b->current->pos];
    }
}

/* Compute the amount of space left in the current element */
static
int32_t flecs_strbuf_memLeftInCurrentElement(
    ecs_strbuf_t *b)
{
    if (b->current->buffer_embedded) {
        return ECS_STRBUF_ELEMENT_SIZE - b->current->pos;
    } else {
        return 0;
    }
}

/* Compute the amount of space left */
static
int32_t flecs_strbuf_memLeft(
    ecs_strbuf_t *b)
{
    if (b->max) {
        return b->max - b->size - b->current->pos;
    } else {
        return INT_MAX;
    }
}

static
void flecs_strbuf_init(
    ecs_strbuf_t *b)
{
    /* Initialize buffer structure only once */
    if (!b->elementCount) {
        b->size = 0;
        b->firstElement.super.next = NULL;
        b->firstElement.super.pos = 0;
        b->firstElement.super.buffer_embedded = true;
        b->firstElement.super.buf = b->firstElement.buf;
        b->elementCount ++;
        b->current = (ecs_strbuf_element*)&b->firstElement;
    }
}

/* Append a format string to a buffer */
static
bool flecs_strbuf_vappend(
    ecs_strbuf_t *b,
    const char* str,
    va_list args)
{
    bool result = true;
    va_list arg_cpy;

    if (!str) {
        return result;
    }

    flecs_strbuf_init(b);

    int32_t memLeftInElement = flecs_strbuf_memLeftInCurrentElement(b);
    int32_t memLeft = flecs_strbuf_memLeft(b);

    if (!memLeft) {
        return false;
    }

    /* Compute the memory required to add the string to the buffer. If user
     * provided buffer, use space left in buffer, otherwise use space left in
     * current element. */
    int32_t max_copy = b->buf ? memLeft : memLeftInElement;
    int32_t memRequired;

    va_copy(arg_cpy, args);
    memRequired = vsnprintf(
        flecs_strbuf_ptr(b), (size_t)(max_copy + 1), str, args);

    ecs_assert(memRequired != -1, ECS_INTERNAL_ERROR, NULL);

    if (memRequired <= memLeftInElement) {
        /* Element was large enough to fit string */
        b->current->pos += memRequired;
    } else if ((memRequired - memLeftInElement) < memLeft) {
        /* If string is a format string, a new buffer of size memRequired is
         * needed to re-evaluate the format string and only use the part that
         * wasn't already copied to the previous element */
        if (memRequired <= ECS_STRBUF_ELEMENT_SIZE) {
            /* Resulting string fits in standard-size buffer. Note that the
             * entire string needs to fit, not just the remainder, as the
             * format string cannot be partially evaluated */
            flecs_strbuf_grow(b);

            /* Copy entire string to new buffer */
            ecs_os_vsprintf(flecs_strbuf_ptr(b), str, arg_cpy);

            /* Ignore the part of the string that was copied into the
             * previous buffer. The string copied into the new buffer could
             * be memmoved so that only the remainder is left, but that is
             * most likely more expensive than just keeping the entire
             * string. */

            /* Update position in buffer */
            b->current->pos += memRequired;
        } else {
            /* Resulting string does not fit in standard-size buffer.
             * Allocate a new buffer that can hold the entire string. */
            char *dst = ecs_os_malloc(memRequired + 1);
            ecs_os_vsprintf(dst, str, arg_cpy);
            flecs_strbuf_grow_str(b, dst, dst, memRequired);
        }
    }

    va_end(arg_cpy);

    return flecs_strbuf_memLeft(b) > 0;
}

static
bool flecs_strbuf_appendstr(
    ecs_strbuf_t *b,
    const char* str,
    int n)
{
    flecs_strbuf_init(b);

    int32_t memLeftInElement = flecs_strbuf_memLeftInCurrentElement(b);
    int32_t memLeft = flecs_strbuf_memLeft(b);
    if (memLeft <= 0) {
        return false;
    }

    /* Never write more than what the buffer can store */
    if (n > memLeft) {
        n = memLeft;
    }

    if (n <= memLeftInElement) {
        /* Element was large enough to fit string */
        ecs_os_strncpy(flecs_strbuf_ptr(b), str, n);
        b->current->pos += n;
    } else if ((n - memLeftInElement) < memLeft) {
        ecs_os_strncpy(flecs_strbuf_ptr(b), str, memLeftInElement);

        /* Element was not large enough, but buffer still has space */
        b->current->pos += memLeftInElement;
        n -= memLeftInElement;

        /* Current element was too small, copy remainder into new element */
        if (n < ECS_STRBUF_ELEMENT_SIZE) {
            /* A standard-size buffer is large enough for the new string */
            flecs_strbuf_grow(b);

            /* Copy the remainder to the new buffer */
            if (n) {
                /* If a max number of characters to write is set, only a
                 * subset of the string should be copied to the buffer */
                ecs_os_strncpy(
                    flecs_strbuf_ptr(b),
                    str + memLeftInElement,
                    (size_t)n);
            } else {
                ecs_os_strcpy(flecs_strbuf_ptr(b), str + memLeftInElement);
            }

            /* Update to number of characters copied to new buffer */
            b->current->pos += n;
        } else {
            /* String doesn't fit in a single element, strdup */
            char *remainder = ecs_os_strdup(str + memLeftInElement);
            flecs_strbuf_grow_str(b, remainder, remainder, n);
        }
    } else {
        /* Buffer max has been reached */
        return false;
    }

    return flecs_strbuf_memLeft(b) > 0;
}

static
bool flecs_strbuf_appendch(
    ecs_strbuf_t *b,
    char ch)
{
    flecs_strbuf_init(b);

    int32_t memLeftInElement = flecs_strbuf_memLeftInCurrentElement(b);
    int32_t memLeft = flecs_strbuf_memLeft(b);
    if (memLeft <= 0) {
        return false;
    }

    if (memLeftInElement) {
        /* Element was large enough to fit string */
        flecs_strbuf_ptr(b)[0] = ch;
        b->current->pos ++;
    } else {
        flecs_strbuf_grow(b);
        flecs_strbuf_ptr(b)[0] = ch;
        b->current->pos ++;
    }

    return flecs_strbuf_memLeft(b) > 0;
}

bool ecs_strbuf_vappend(
    ecs_strbuf_t *b,
    const char* fmt,
    va_list args)
{
    ecs_assert(b != NULL, ECS_INVALID_PARAMETER, NULL);
    ecs_assert(fmt != NULL, ECS_INVALID_PARAMETER, NULL);
    return flecs_strbuf_vappend(b, fmt, args);
}

bool ecs_strbuf_append(
    ecs_strbuf_t *b,
    const char* fmt,
    ...)
{
    ecs_assert(b != NULL, ECS_INVALID_PARAMETER, NULL);
    ecs_assert(fmt != NULL, ECS_INVALID_PARAMETER, NULL);

    va_list args;
    va_start(args, fmt);
    bool result = flecs_strbuf_vappend(b, fmt, args);
    va_end(args);

    return result;
}

bool ecs_strbuf_appendstrn(
    ecs_strbuf_t *b,
    const char* str,
    int32_t len)
{
    ecs_assert(b != NULL, ECS_INVALID_PARAMETER, NULL);
    ecs_assert(str != NULL, ECS_INVALID_PARAMETER, NULL);
    return flecs_strbuf_appendstr(b, str, len);
}

bool ecs_strbuf_appendch(
    ecs_strbuf_t *b,
    char ch)
{
    ecs_assert(b != NULL, ECS_INVALID_PARAMETER, NULL); 
    return flecs_strbuf_appendch(b, ch);
}

bool ecs_strbuf_appendint(
    ecs_strbuf_t *b,
    int64_t v)
{
    ecs_assert(b != NULL, ECS_INVALID_PARAMETER, NULL); 
    char numbuf[32];
    char *ptr = flecs_strbuf_itoa(numbuf, v);
    return ecs_strbuf_appendstrn(b, numbuf, flecs_ito(int32_t, ptr - numbuf));
}

bool ecs_strbuf_appendflt(
    ecs_strbuf_t *b,
    double flt,
    char nan_delim)
{
    ecs_assert(b != NULL, ECS_INVALID_PARAMETER, NULL); 
    return flecs_strbuf_ftoa(b, flt, 10, nan_delim);
}

bool ecs_strbuf_appendstr_zerocpy(
    ecs_strbuf_t *b,
    char* str)
{
    ecs_assert(b != NULL, ECS_INVALID_PARAMETER, NULL);
    ecs_assert(str != NULL, ECS_INVALID_PARAMETER, NULL);
    flecs_strbuf_init(b);
    flecs_strbuf_grow_str(b, str, str, 0);
    return true;
}

bool ecs_strbuf_appendstr_zerocpyn(
    ecs_strbuf_t *b,
    char *str,
    int32_t n)
{
    ecs_assert(b != NULL, ECS_INVALID_PARAMETER, NULL);
    ecs_assert(str != NULL, ECS_INVALID_PARAMETER, NULL);
    flecs_strbuf_init(b);
    flecs_strbuf_grow_str(b, str, str, n);
    return true;
}

bool ecs_strbuf_appendstr_zerocpy_const(
    ecs_strbuf_t *b,
    const char* str)
{
    ecs_assert(b != NULL, ECS_INVALID_PARAMETER, NULL);
    ecs_assert(str != NULL, ECS_INVALID_PARAMETER, NULL);
    /* Removes const modifier, but logic prevents changing / delete string */
    flecs_strbuf_init(b);
    flecs_strbuf_grow_str(b, (char*)str, NULL, 0);
    return true;
}

bool ecs_strbuf_appendstr_zerocpyn_const(
    ecs_strbuf_t *b,
    const char *str,
    int32_t n)
{
    ecs_assert(b != NULL, ECS_INVALID_PARAMETER, NULL);
    ecs_assert(str != NULL, ECS_INVALID_PARAMETER, NULL);
    /* Removes const modifier, but logic prevents changing / delete string */
    flecs_strbuf_init(b);
    flecs_strbuf_grow_str(b, (char*)str, NULL, n);
    return true;
}

bool ecs_strbuf_appendstr(
    ecs_strbuf_t *b,
    const char* str)
{
    ecs_assert(b != NULL, ECS_INVALID_PARAMETER, NULL);
    ecs_assert(str != NULL, ECS_INVALID_PARAMETER, NULL);
    return flecs_strbuf_appendstr(b, str, ecs_os_strlen(str));
}

bool ecs_strbuf_mergebuff(
    ecs_strbuf_t *dst_buffer,
    ecs_strbuf_t *src_buffer)
{
    if (src_buffer->elementCount) {
        if (src_buffer->buf) {
            return ecs_strbuf_appendstrn(
                dst_buffer, src_buffer->buf, src_buffer->length);
        } else {
            ecs_strbuf_element *e = (ecs_strbuf_element*)&src_buffer->firstElement;

            /* Copy first element as it is inlined in the src buffer */
            ecs_strbuf_appendstrn(dst_buffer, e->buf, e->pos);

            while ((e = e->next)) {
                dst_buffer->current->next = ecs_os_malloc(sizeof(ecs_strbuf_element));
                *dst_buffer->current->next = *e;
            }
        }

        *src_buffer = ECS_STRBUF_INIT;
    }

    return true;
}

char* ecs_strbuf_get(
    ecs_strbuf_t *b) 
{
    ecs_assert(b != NULL, ECS_INVALID_PARAMETER, NULL);

    char* result = NULL;
    if (b->elementCount) {
        if (b->buf) {
            b->buf[b->current->pos] = '\0';
            result = ecs_os_strdup(b->buf);
        } else {
            void *next = NULL;
            int32_t len = b->size + b->current->pos + 1;
            ecs_strbuf_element *e = (ecs_strbuf_element*)&b->firstElement;

            result = ecs_os_malloc(len);
            char* ptr = result;

            do {
                ecs_os_memcpy(ptr, e->buf, e->pos);
                ptr += e->pos;
                next = e->next;
                if (e != &b->firstElement.super) {
                    if (!e->buffer_embedded) {
                        ecs_os_free(((ecs_strbuf_element_str*)e)->alloc_str);
                    }
                    ecs_os_free(e);
                }
            } while ((e = next));

            result[len - 1] = '\0';
            b->length = len;
        }
    } else {
        result = NULL;
    }

    b->elementCount = 0;

    b->content = result;

    return result;
}

char *ecs_strbuf_get_small(
    ecs_strbuf_t *b)
{
    ecs_assert(b != NULL, ECS_INVALID_PARAMETER, NULL);

    int32_t written = ecs_strbuf_written(b);
    ecs_assert(written <= ECS_STRBUF_ELEMENT_SIZE, ECS_INVALID_OPERATION, NULL);
    char *buf = b->firstElement.buf;
    buf[written] = '\0';
    return buf;
}

void ecs_strbuf_reset(
    ecs_strbuf_t *b) 
{
    ecs_assert(b != NULL, ECS_INVALID_PARAMETER, NULL);

    if (b->elementCount && !b->buf) {
        void *next = NULL;
        ecs_strbuf_element *e = (ecs_strbuf_element*)&b->firstElement;
        do {
            next = e->next;
            if (e != (ecs_strbuf_element*)&b->firstElement) {
                ecs_os_free(e);
            }
        } while ((e = next));
    }

    *b = ECS_STRBUF_INIT;
}

void ecs_strbuf_list_push(
    ecs_strbuf_t *b,
    const char *list_open,
    const char *separator)
{
    ecs_assert(b != NULL, ECS_INVALID_PARAMETER, NULL);
    ecs_assert(list_open != NULL, ECS_INVALID_PARAMETER, NULL);
    ecs_assert(separator != NULL, ECS_INVALID_PARAMETER, NULL);
    ecs_assert(b->list_sp >= 0, ECS_INVALID_OPERATION, NULL);
    b->list_sp ++;
    ecs_assert(b->list_sp < ECS_STRBUF_MAX_LIST_DEPTH, 
        ECS_INVALID_OPERATION, NULL);

    b->list_stack[b->list_sp].count = 0;
    b->list_stack[b->list_sp].separator = separator;

    if (list_open) {
        char ch = list_open[0];
        if (ch && !list_open[1]) {
            ecs_strbuf_appendch(b, ch);
        } else {
            ecs_strbuf_appendstr(b, list_open);
        }
    }
}

void ecs_strbuf_list_pop(
    ecs_strbuf_t *b,
    const char *list_close)
{
    ecs_assert(b != NULL, ECS_INVALID_PARAMETER, NULL);
    ecs_assert(list_close != NULL, ECS_INVALID_PARAMETER, NULL);
    ecs_assert(b->list_sp > 0, ECS_INVALID_OPERATION, NULL);

    b->list_sp --;
    
    if (list_close) {
        char ch = list_close[0];
        if (ch && !list_close[1]) {
            ecs_strbuf_appendch(b, list_close[0]);
        } else {
            ecs_strbuf_appendstr(b, list_close);
        }
    }
}

void ecs_strbuf_list_next(
    ecs_strbuf_t *b)
{
    ecs_assert(b != NULL, ECS_INVALID_PARAMETER, NULL);

    int32_t list_sp = b->list_sp;
    if (b->list_stack[list_sp].count != 0) {
        const char *sep = b->list_stack[list_sp].separator;
        if (sep && !sep[1]) {
            ecs_strbuf_appendch(b, sep[0]);
        } else {
            ecs_strbuf_appendstr(b, sep);
        }
    }
    b->list_stack[list_sp].count ++;
}

bool ecs_strbuf_list_appendch(
    ecs_strbuf_t *b,
    char ch)
{
    ecs_assert(b != NULL, ECS_INVALID_PARAMETER, NULL);
    ecs_strbuf_list_next(b);
    return flecs_strbuf_appendch(b, ch);
}

bool ecs_strbuf_list_append(
    ecs_strbuf_t *b,
    const char *fmt,
    ...)
{
    ecs_assert(b != NULL, ECS_INVALID_PARAMETER, NULL);
    ecs_assert(fmt != NULL, ECS_INVALID_PARAMETER, NULL);

    ecs_strbuf_list_next(b);

    va_list args;
    va_start(args, fmt);
    bool result = flecs_strbuf_vappend(b, fmt, args);
    va_end(args);

    return result;
}

bool ecs_strbuf_list_appendstr(
    ecs_strbuf_t *b,
    const char *str)
{
    ecs_assert(b != NULL, ECS_INVALID_PARAMETER, NULL);
    ecs_assert(str != NULL, ECS_INVALID_PARAMETER, NULL);

    ecs_strbuf_list_next(b);
    return ecs_strbuf_appendstr(b, str);
}

bool ecs_strbuf_list_appendstrn(
    ecs_strbuf_t *b,
    const char *str,
    int32_t n)
{
    ecs_assert(b != NULL, ECS_INVALID_PARAMETER, NULL);
    ecs_assert(str != NULL, ECS_INVALID_PARAMETER, NULL);

    ecs_strbuf_list_next(b);
    return ecs_strbuf_appendstrn(b, str, n);
}

int32_t ecs_strbuf_written(
    const ecs_strbuf_t *b)
{
    ecs_assert(b != NULL, ECS_INVALID_PARAMETER, NULL);
    if (b->current) {
        return b->size + b->current->pos;
    } else {
        return 0;
    }
}

/**
 * @file datastructures/vec.c
 * @brief Vector with allocator support.
 */


ecs_vec_t* ecs_vec_init(
    ecs_allocator_t *allocator,
    ecs_vec_t *v,
    ecs_size_t size,
    int32_t elem_count)
{
    ecs_assert(size != 0, ECS_INVALID_PARAMETER, NULL);
    v->array = NULL;
    v->count = 0;
    if (elem_count) {
        if (allocator) {
            v->array = flecs_alloc(allocator, size * elem_count);
        } else {
            v->array = ecs_os_malloc(size * elem_count);
        }
    }
    v->size = elem_count;
#ifdef FLECS_DEBUG
    v->elem_size = size;
#endif
    return v;
}

void ecs_vec_init_if(
    ecs_vec_t *vec,
    ecs_size_t size)
{
    ecs_dbg_assert(!vec->elem_size || vec->elem_size == size, ECS_INVALID_PARAMETER, NULL);
    (void)vec;
    (void)size;
#ifdef FLECS_DEBUG
    if (!vec->elem_size) {
        ecs_assert(vec->count == 0, ECS_INTERNAL_ERROR, NULL);
        ecs_assert(vec->size == 0, ECS_INTERNAL_ERROR, NULL);
        ecs_assert(vec->array == NULL, ECS_INTERNAL_ERROR, NULL);
        vec->elem_size = size;
    }
#endif
}

void ecs_vec_fini(
    ecs_allocator_t *allocator,
    ecs_vec_t *v,
    ecs_size_t size)
{
    if (v->array) {
        ecs_dbg_assert(!size || size == v->elem_size, ECS_INVALID_PARAMETER, NULL);
        if (allocator) {
            flecs_free(allocator, size * v->size, v->array);
        } else {
            ecs_os_free(v->array);
        }
        v->array = NULL;
        v->count = 0;
        v->size = 0;
    }
}

ecs_vec_t* ecs_vec_reset(
    ecs_allocator_t *allocator,
    ecs_vec_t *v,
    ecs_size_t size)
{
    if (!v->size) {
        ecs_vec_init(allocator, v, size, 0);
    } else {
        ecs_dbg_assert(size == v->elem_size, ECS_INTERNAL_ERROR, NULL);
        ecs_vec_clear(v);
    }
    return v;
}

void ecs_vec_clear(
    ecs_vec_t *vec)
{
    vec->count = 0;
}

ecs_vec_t ecs_vec_copy(
    ecs_allocator_t *allocator,
    ecs_vec_t *v,
    ecs_size_t size)
{
    ecs_dbg_assert(size == v->elem_size, ECS_INVALID_PARAMETER, NULL);
    void *array;
    if (allocator) {
        array = flecs_dup(allocator, size * v->size, v->array);
    } else {
        array = ecs_os_memdup(v->array, size * v->size);
    }
    return (ecs_vec_t) {
        .count = v->count,
        .size = v->size,
        .array = array
#ifdef FLECS_DEBUG
        , .elem_size = size
#endif
    };
}

void ecs_vec_reclaim(
    ecs_allocator_t *allocator,
    ecs_vec_t *v,
    ecs_size_t size)
{
    ecs_dbg_assert(size == v->elem_size, ECS_INVALID_PARAMETER, NULL);
    int32_t count = v->count;
    if (count < v->size) {
        if (count) {
            if (allocator) {
                v->array = flecs_realloc(
                    allocator, size * count, size * v->size, v->array);
            } else {
                v->array = ecs_os_realloc(v->array, size * count);
            }
            v->size = count;
        } else {
            ecs_vec_fini(allocator, v, size);
        }
    }
}

void ecs_vec_set_size(
    ecs_allocator_t *allocator,
    ecs_vec_t *v,
    ecs_size_t size,
    int32_t elem_count)
{
    ecs_dbg_assert(size == v->elem_size, ECS_INVALID_PARAMETER, NULL);
    if (v->size != elem_count) {
        if (elem_count < v->count) {
            elem_count = v->count;
        }

        elem_count = flecs_next_pow_of_2(elem_count);
        if (elem_count < 2) {
            elem_count = 2;
        }
        if (elem_count != v->size) {
            if (allocator) {
                v->array = flecs_realloc(
                    allocator, size * elem_count, size * v->size, v->array);
            } else {
                v->array = ecs_os_realloc(v->array, size * elem_count);
            }
            v->size = elem_count;
        }
    }
}

void ecs_vec_set_min_size(
    struct ecs_allocator_t *allocator,
    ecs_vec_t *vec,
    ecs_size_t size,
    int32_t elem_count)
{
    if (elem_count > vec->size) {
        ecs_vec_set_size(allocator, vec, size, elem_count);
    }
}

void ecs_vec_set_min_count(
    struct ecs_allocator_t *allocator,
    ecs_vec_t *vec,
    ecs_size_t size,
    int32_t elem_count)
{
    ecs_vec_set_min_size(allocator, vec, size, elem_count);
    if (vec->count < elem_count) {
        vec->count = elem_count;
    }
}

void ecs_vec_set_min_count_zeromem(
    struct ecs_allocator_t *allocator,
    ecs_vec_t *vec,
    ecs_size_t size,
    int32_t elem_count)
{
    int32_t count = vec->count;
    if (count < elem_count) {
        ecs_vec_set_min_count(allocator, vec, size, elem_count);
        ecs_os_memset(ECS_ELEM(vec->array, size, count), 0, 
            size * (elem_count - count));
    }
}

void ecs_vec_set_count(
    ecs_allocator_t *allocator,
    ecs_vec_t *v,
    ecs_size_t size,
    int32_t elem_count)
{
    ecs_dbg_assert(size == v->elem_size, ECS_INVALID_PARAMETER, NULL);
    if (v->count != elem_count) {
        if (v->size < elem_count) {
            ecs_vec_set_size(allocator, v, size, elem_count);
        }

        v->count = elem_count;
    }
}

void* ecs_vec_grow(
    ecs_allocator_t *allocator,
    ecs_vec_t *v,
    ecs_size_t size,
    int32_t elem_count)
{
    ecs_dbg_assert(size == v->elem_size, ECS_INVALID_PARAMETER, NULL);
    ecs_assert(elem_count > 0, ECS_INTERNAL_ERROR, NULL);
    int32_t count = v->count;
    ecs_vec_set_count(allocator, v, size, count + elem_count);
    return ECS_ELEM(v->array, size, count);
}

void* ecs_vec_append(
    ecs_allocator_t *allocator,
    ecs_vec_t *v,
    ecs_size_t size)
{
    ecs_dbg_assert(size == v->elem_size, ECS_INVALID_PARAMETER, NULL);
    int32_t count = v->count;
    if (v->size == count) {
        ecs_vec_set_size(allocator, v, size, count + 1);
    }
    v->count = count + 1;
    return ECS_ELEM(v->array, size, count);
}

void ecs_vec_remove(
    ecs_vec_t *v,
    ecs_size_t size,
    int32_t index)
{
    ecs_dbg_assert(size == v->elem_size, ECS_INVALID_PARAMETER, NULL);
    ecs_assert(index < v->count, ECS_OUT_OF_RANGE, NULL);
    if (index == --v->count) {
        return;
    }

    ecs_os_memcpy(
        ECS_ELEM(v->array, size, index),
        ECS_ELEM(v->array, size, v->count),
        size);
}

void ecs_vec_remove_last(
    ecs_vec_t *v)
{
    v->count --;
}

int32_t ecs_vec_count(
    const ecs_vec_t *v)
{
    return v->count;
}

int32_t ecs_vec_size(
    const ecs_vec_t *v)
{
    return v->size;
}

void* ecs_vec_get(
    const ecs_vec_t *v,
    ecs_size_t size,
    int32_t index)
{
    ecs_dbg_assert(size == v->elem_size, ECS_INVALID_PARAMETER, NULL);
    ecs_assert(index < v->count, ECS_OUT_OF_RANGE, NULL);
    return ECS_ELEM(v->array, size, index);
}

void* ecs_vec_last(
    const ecs_vec_t *v,
    ecs_size_t size)
{
    ecs_dbg_assert(!v->elem_size || size == v->elem_size, 
        ECS_INVALID_PARAMETER, NULL);
    return ECS_ELEM(v->array, size, v->count - 1);
}

void* ecs_vec_first(
    const ecs_vec_t *v)
{
    return v->array;
}

/**
 * @file datastructures/map.c
 * @brief Map data structure.
 * 
 * Map data structure for 64bit keys and dynamic payload size.
 */


/* The ratio used to determine whether the map should flecs_map_rehash. If
 * (element_count * ECS_LOAD_FACTOR) > bucket_count, bucket count is increased. */
#define ECS_LOAD_FACTOR (12)
#define ECS_BUCKET_END(b, c) ECS_ELEM_T(b, ecs_bucket_t, c)

static
uint8_t flecs_log2(uint32_t v) {
    static const uint8_t log2table[32] = 
        {0, 9,  1,  10, 13, 21, 2,  29, 11, 14, 16, 18, 22, 25, 3, 30,
         8, 12, 20, 28, 15, 17, 24, 7,  19, 27, 23, 6,  26, 5,  4, 31};

    v |= v >> 1;
    v |= v >> 2;
    v |= v >> 4;
    v |= v >> 8;
    v |= v >> 16;
    return log2table[(uint32_t)(v * 0x07C4ACDDU) >> 27];
}

/* Get bucket count for number of elements */
static
int32_t flecs_map_get_bucket_count(
    int32_t count)
{
    return flecs_next_pow_of_2((int32_t)(count * ECS_LOAD_FACTOR * 0.1));
}

/* Get bucket shift amount for a given bucket count */
static
uint8_t flecs_map_get_bucket_shift (
    int32_t bucket_count)
{
    return (uint8_t)(64u - flecs_log2((uint32_t)bucket_count));
}

/* Get bucket index for provided map key */
static
int32_t flecs_map_get_bucket_index(
    uint16_t bucket_shift,
    ecs_map_key_t key) 
{
    ecs_assert(bucket_shift != 0, ECS_INTERNAL_ERROR, NULL);
    return (int32_t)((11400714819323198485ull * key) >> bucket_shift);
}

/* Get bucket for key */
static
ecs_bucket_t* flecs_map_get_bucket(
    const ecs_map_t *map,
    ecs_map_key_t key)
{
    ecs_assert(map != NULL, ECS_INVALID_PARAMETER, NULL);
    int32_t bucket_id = flecs_map_get_bucket_index(map->bucket_shift, key);
    ecs_assert(bucket_id < map->bucket_count, ECS_INTERNAL_ERROR, NULL);
    return &map->buckets[bucket_id];
}

/* Add element to bucket */
static
ecs_map_val_t* flecs_map_bucket_add(
    ecs_block_allocator_t *allocator,
    ecs_bucket_t *bucket,
    ecs_map_key_t key)
{
    ecs_bucket_entry_t *new_entry = flecs_balloc(allocator);
    new_entry->key = key;
    new_entry->next = bucket->first;
    bucket->first = new_entry;
    return &new_entry->value;
}

/* Remove element from bucket */
static
ecs_map_val_t flecs_map_bucket_remove(
    ecs_map_t *map,
    ecs_bucket_t *bucket,
    ecs_map_key_t key)
{
    ecs_bucket_entry_t *entry;
    for (entry = bucket->first; entry; entry = entry->next) {
        if (entry->key == key) {
            ecs_map_val_t value = entry->value;
            ecs_bucket_entry_t **next_holder = &bucket->first;
            while(*next_holder != entry) {
                next_holder = &(*next_holder)->next;
            }
            *next_holder = entry->next;
            flecs_bfree(map->entry_allocator, entry);
            map->count --;
            return value;
        }
    }
    
    return 0;
}

/* Free contents of bucket */
static
void flecs_map_bucket_clear(
    ecs_block_allocator_t *allocator,
    ecs_bucket_t *bucket)
{
    ecs_bucket_entry_t *entry = bucket->first;
    while(entry) {
        ecs_bucket_entry_t *next = entry->next;
        flecs_bfree(allocator, entry);
        entry = next;
    }
}

/* Get payload pointer for key from bucket */
static
ecs_map_val_t* flecs_map_bucket_get(
    ecs_bucket_t *bucket,
    ecs_map_key_t key)
{
    ecs_bucket_entry_t *entry;
    for (entry = bucket->first; entry; entry = entry->next) {
        if (entry->key == key) {
            return &entry->value;
        }
    }
    return NULL;
}

/* Grow number of buckets */
static
void flecs_map_rehash(
    ecs_map_t *map,
    int32_t count)
{
    count = flecs_next_pow_of_2(count);
    if (count < 2) {
        count = 2;
    }
    ecs_assert(count > map->bucket_count, ECS_INTERNAL_ERROR, NULL);
    
    int32_t old_count = map->bucket_count;
    ecs_bucket_t *buckets = map->buckets, *b, *end = ECS_BUCKET_END(buckets, old_count);

    if (map->allocator) {
        map->buckets = flecs_calloc_n(map->allocator, ecs_bucket_t, count);
    } else {
        map->buckets = ecs_os_calloc_n(ecs_bucket_t, count);
    }
    map->bucket_count = count;
    map->bucket_shift = flecs_map_get_bucket_shift(count);

    /* Remap old bucket entries to new buckets */
    for (b = buckets; b < end; b++) {
        ecs_bucket_entry_t* entry;
        for (entry = b->first; entry;) {
            ecs_bucket_entry_t* next = entry->next;
            int32_t bucket_index = flecs_map_get_bucket_index(
                map->bucket_shift, entry->key);
            ecs_bucket_t *bucket = &map->buckets[bucket_index];
            entry->next = bucket->first;
            bucket->first = entry;
            entry = next;
        }
    }

    if (map->allocator) {
        flecs_free_n(map->allocator, ecs_bucket_t, old_count, buckets);
    } else {
        ecs_os_free(buckets);
    }
}

void ecs_map_params_init(
    ecs_map_params_t *params,
    ecs_allocator_t *allocator)
{
    params->allocator = allocator;
    flecs_ballocator_init_t(&params->entry_allocator, ecs_bucket_entry_t);
}

void ecs_map_params_fini(
    ecs_map_params_t *params)
{
    flecs_ballocator_fini(&params->entry_allocator);
}

void ecs_map_init_w_params(
    ecs_map_t *result,
    ecs_map_params_t *params)
{
    ecs_os_zeromem(result);

    result->allocator = params->allocator;

    if (params->entry_allocator.chunk_size) {
        result->entry_allocator = &params->entry_allocator;
        result->shared_allocator = true;
    } else {
        result->entry_allocator = flecs_ballocator_new_t(ecs_bucket_entry_t);
    }

    flecs_map_rehash(result, 0);
}

void ecs_map_init_w_params_if(
    ecs_map_t *result,
    ecs_map_params_t *params)
{
    if (!ecs_map_is_init(result)) {
        ecs_map_init_w_params(result, params);
    }
}

void ecs_map_init(
    ecs_map_t *result,
    ecs_allocator_t *allocator)
{
    ecs_map_init_w_params(result, &(ecs_map_params_t) {
        .allocator = allocator
    });
}

void ecs_map_init_if(
    ecs_map_t *result,
    ecs_allocator_t *allocator)
{
    if (!ecs_map_is_init(result)) {
        ecs_map_init(result, allocator);
    }   
}

void ecs_map_fini(
    ecs_map_t *map)
{
    if (!ecs_map_is_init(map)) {
        return;
    }

    bool sanitize = false;
#ifdef FLECS_SANITIZE
    sanitize = true;
#endif

    /* Free buckets in sanitized mode, so we can replace the allocator with
     * regular malloc/free and use asan/valgrind to find memory errors. */
    ecs_allocator_t *a = map->allocator;
    ecs_block_allocator_t *ea = map->entry_allocator;
    if (map->shared_allocator || sanitize) {
        ecs_bucket_t *bucket = map->buckets, *end = &bucket[map->bucket_count];
        while (bucket != end) {
            flecs_map_bucket_clear(ea, bucket);
            bucket ++;
        }
    }

    if (ea && !map->shared_allocator) {
        flecs_ballocator_free(ea);
        map->entry_allocator = NULL;
    }
    if (a) {
        flecs_free_n(a, ecs_bucket_t, map->bucket_count, map->buckets);
    } else {
        ecs_os_free(map->buckets);
    }

    map->bucket_shift = 0;
}

ecs_map_val_t* ecs_map_get(
    const ecs_map_t *map,
    ecs_map_key_t key)
{
    return flecs_map_bucket_get(flecs_map_get_bucket(map, key), key);
}

void* _ecs_map_get_deref(
    const ecs_map_t *map,
    ecs_map_key_t key)
{
    ecs_map_val_t* ptr = flecs_map_bucket_get(
        flecs_map_get_bucket(map, key), key);
    if (ptr) {
        return (void*)ptr[0];
    }
    return NULL;
}

void ecs_map_insert(
    ecs_map_t *map,
    ecs_map_key_t key,
    ecs_map_val_t value)
{
    ecs_assert(ecs_map_get(map, key) == NULL, ECS_INVALID_PARAMETER, NULL);
    int32_t map_count = ++map->count;
    int32_t tgt_bucket_count = flecs_map_get_bucket_count(map_count);
    int32_t bucket_count = map->bucket_count;
    if (tgt_bucket_count > bucket_count) {
        flecs_map_rehash(map, tgt_bucket_count);
    }

    ecs_bucket_t *bucket = flecs_map_get_bucket(map, key);
    flecs_map_bucket_add(map->entry_allocator, bucket, key)[0] = value;
}

void* ecs_map_insert_alloc(
    ecs_map_t *map,
    ecs_size_t elem_size,
    ecs_map_key_t key)
{
    void *elem = ecs_os_calloc(elem_size);
    ecs_map_insert_ptr(map, key, elem);
    return elem;
}

ecs_map_val_t* ecs_map_ensure(
    ecs_map_t *map,
    ecs_map_key_t key)
{
    ecs_bucket_t *bucket = flecs_map_get_bucket(map, key);
    ecs_map_val_t *result = flecs_map_bucket_get(bucket, key);
    if (result) {
        return result;
    }

    int32_t map_count = ++map->count;
    int32_t tgt_bucket_count = flecs_map_get_bucket_count(map_count);
    int32_t bucket_count = map->bucket_count;
    if (tgt_bucket_count > bucket_count) {
        flecs_map_rehash(map, tgt_bucket_count);
        bucket = flecs_map_get_bucket(map, key);
    }

    ecs_map_val_t* v = flecs_map_bucket_add(map->entry_allocator, bucket, key);
    *v = 0;
    return v;
}

void* ecs_map_ensure_alloc(
    ecs_map_t *map,
    ecs_size_t elem_size,
    ecs_map_key_t key)
{
    ecs_map_val_t *val = ecs_map_ensure(map, key);
    if (!*val) {
        void *elem = ecs_os_calloc(elem_size);
        *val = (ecs_map_val_t)elem;
        return elem;
    } else {
        return (void*)*val;
    }
}

ecs_map_val_t ecs_map_remove(
    ecs_map_t *map,
    ecs_map_key_t key)
{
    return flecs_map_bucket_remove(map, flecs_map_get_bucket(map, key), key);
}

void ecs_map_remove_free(
    ecs_map_t *map,
    ecs_map_key_t key)
{
    ecs_map_val_t val = ecs_map_remove(map, key);
    if (val) {
        ecs_os_free((void*)val);
    }
}

void ecs_map_clear(
    ecs_map_t *map)
{
    ecs_assert(map != NULL, ECS_INVALID_PARAMETER, NULL);
    int32_t i, count = map->bucket_count;
    for (i = 0; i < count; i ++) {
        flecs_map_bucket_clear(map->entry_allocator, &map->buckets[i]);
    }
    if (map->allocator) {
        flecs_free_n(map->allocator, ecs_bucket_t, count, map->buckets);
    } else {
        ecs_os_free(map->buckets);
    }
    map->buckets = NULL;
    map->bucket_count = 0;
    map->count = 0;
    flecs_map_rehash(map, 2);
}

ecs_map_iter_t ecs_map_iter(
    const ecs_map_t *map)
{
    if (ecs_map_is_init(map)) {
        return (ecs_map_iter_t){
            .map = map,
            .bucket = NULL,
            .entry = NULL
        };
    } else {
        return (ecs_map_iter_t){ 0 };
    }
}

bool ecs_map_next(
    ecs_map_iter_t *iter)
{
    const ecs_map_t *map = iter->map;
    ecs_bucket_t *end;
    if (!map || (iter->bucket == (end = &map->buckets[map->bucket_count]))) {
        return false;
    }

    ecs_bucket_entry_t *entry = NULL;
    if (!iter->bucket) {
        for (iter->bucket = map->buckets; 
            iter->bucket != end;
            ++iter->bucket) 
        {
            if (iter->bucket->first) {
                entry = iter->bucket->first;
                break;
            }
        }
        if (iter->bucket == end) {
            return false;
        }
    } else if ((entry = iter->entry) == NULL) {
        do {
            ++iter->bucket;
            if (iter->bucket == end) {
                return false;
            }
        } while(!iter->bucket->first);
        entry = iter->bucket->first;
    }

    ecs_assert(entry != NULL, ECS_INTERNAL_ERROR, NULL);
    iter->entry = entry->next;
    iter->res = &entry->key;

    return true;
}

void ecs_map_copy(
    ecs_map_t *dst,
    const ecs_map_t *src)
{
    if (ecs_map_is_init(dst)) {
        ecs_assert(ecs_map_count(dst) == 0, ECS_INVALID_PARAMETER, NULL);
        ecs_map_fini(dst);
    }
    
    if (!ecs_map_is_init(src)) {
        return;
    }

    ecs_map_init(dst, src->allocator);

    ecs_map_iter_t it = ecs_map_iter(src);
    while (ecs_map_next(&it)) {
        ecs_map_insert(dst, ecs_map_key(&it), ecs_map_value(&it));
    }
}

/**
 * @file datastructures/block_allocator.c
 * @brief Block allocator.
 * 
 * A block allocator is an allocator for a fixed size that allocates blocks of
 * memory with N elements of the requested size.
 */


// #ifdef FLECS_SANITIZE
// #define FLECS_MEMSET_UNINITIALIZED
// #endif

int64_t ecs_block_allocator_alloc_count = 0;
int64_t ecs_block_allocator_free_count = 0;

static
ecs_block_allocator_chunk_header_t* flecs_balloc_block(
    ecs_block_allocator_t *allocator)
{
    if (!allocator->chunk_size) {
        return NULL;
    }

    ecs_block_allocator_block_t *block = 
        ecs_os_malloc(ECS_SIZEOF(ecs_block_allocator_block_t) +
            allocator->block_size);
    ecs_block_allocator_chunk_header_t *first_chunk = ECS_OFFSET(block, 
        ECS_SIZEOF(ecs_block_allocator_block_t));

    block->memory = first_chunk;
    if (!allocator->block_tail) {
        ecs_assert(!allocator->block_head, ECS_INTERNAL_ERROR, 0);
        block->next = NULL;
        allocator->block_head = block;
        allocator->block_tail = block;
    } else {
        block->next = NULL;
        allocator->block_tail->next = block;
        allocator->block_tail = block;
    }

    ecs_block_allocator_chunk_header_t *chunk = first_chunk;
    int32_t i, end;
    for (i = 0, end = allocator->chunks_per_block - 1; i < end; ++i) {
        chunk->next = ECS_OFFSET(chunk, allocator->chunk_size);
        chunk = chunk->next;
    }

    ecs_os_linc(&ecs_block_allocator_alloc_count);

    chunk->next = NULL;
    return first_chunk;
}

void flecs_ballocator_init(
    ecs_block_allocator_t *ba,
    ecs_size_t size)
{
    ecs_assert(ba != NULL, ECS_INTERNAL_ERROR, NULL);
    ecs_assert(size != 0, ECS_INTERNAL_ERROR, NULL);
    ba->data_size = size;
#ifdef FLECS_SANITIZE
    size += ECS_SIZEOF(int64_t);
#endif
    ba->chunk_size = ECS_ALIGN(size, 16);
    ba->chunks_per_block = ECS_MAX(4096 / ba->chunk_size, 1);
    ba->block_size = ba->chunks_per_block * ba->chunk_size;
    ba->head = NULL;
    ba->block_head = NULL;
    ba->block_tail = NULL;
}

ecs_block_allocator_t* flecs_ballocator_new(
    ecs_size_t size)
{
    ecs_block_allocator_t *result = ecs_os_calloc_t(ecs_block_allocator_t);
    flecs_ballocator_init(result, size);
    return result;
}

void flecs_ballocator_fini(
    ecs_block_allocator_t *ba)
{
    ecs_assert(ba != NULL, ECS_INTERNAL_ERROR, NULL);

#ifdef FLECS_SANITIZE
    ecs_assert(ba->alloc_count == 0, ECS_LEAK_DETECTED, NULL);
#endif

    ecs_block_allocator_block_t *block;
    for (block = ba->block_head; block;) {
        ecs_block_allocator_block_t *next = block->next;
        ecs_os_free(block);
        ecs_os_linc(&ecs_block_allocator_free_count);
        block = next;
    }
    ba->block_head = NULL;
}

void flecs_ballocator_free(
    ecs_block_allocator_t *ba)
{
    flecs_ballocator_fini(ba);
    ecs_os_free(ba);
}

void* flecs_balloc(
    ecs_block_allocator_t *ba) 
{
    void *result;
#ifdef FLECS_USE_OS_ALLOC
    result = ecs_os_malloc(ba->data_size);
#else

    if (!ba) return NULL;

    if (!ba->head) {
        ba->head = flecs_balloc_block(ba);
    }

    result = ba->head;
    ba->head = ba->head->next;

#ifdef FLECS_SANITIZE
    ecs_assert(ba->alloc_count >= 0, ECS_INTERNAL_ERROR, "corrupted allocator");
    ba->alloc_count ++;
    *(int64_t*)result = ba->chunk_size;
    result = ECS_OFFSET(result, ECS_SIZEOF(int64_t));
#endif
#endif

#ifdef FLECS_MEMSET_UNINITIALIZED
    ecs_os_memset(result, 0xAA, ba->data_size);
#endif

    return result;
}

void* flecs_bcalloc(
    ecs_block_allocator_t *ba) 
{
#ifdef FLECS_USE_OS_ALLOC
    return ecs_os_calloc(ba->data_size);
#endif

    if (!ba) return NULL;
    void *result = flecs_balloc(ba);
    ecs_os_memset(result, 0, ba->data_size);
    return result;
}

void flecs_bfree(
    ecs_block_allocator_t *ba, 
    void *memory) 
{
#ifdef FLECS_USE_OS_ALLOC
    ecs_os_free(memory);
    return;
#endif

    if (!ba) {
        ecs_assert(memory == NULL, ECS_INTERNAL_ERROR, NULL);
        return;
    }
    if (memory == NULL) {
        return;
    }

#ifdef FLECS_SANITIZE
    memory = ECS_OFFSET(memory, -ECS_SIZEOF(int64_t));
    if (*(int64_t*)memory != ba->chunk_size) {
        ecs_err("chunk %p returned to wrong allocator "
            "(chunk = %ub, allocator = %ub)",
                memory, *(int64_t*)memory, ba->chunk_size);
        ecs_abort(ECS_INTERNAL_ERROR, NULL);
    }

    ba->alloc_count --;
#endif

    ecs_block_allocator_chunk_header_t *chunk = memory;
    chunk->next = ba->head;
    ba->head = chunk;
    ecs_assert(ba->alloc_count >= 0, ECS_INTERNAL_ERROR, "corrupted allocator");
}

void* flecs_brealloc(
    ecs_block_allocator_t *dst, 
    ecs_block_allocator_t *src, 
    void *memory)
{
    void *result;
#ifdef FLECS_USE_OS_ALLOC
    result = ecs_os_realloc(memory, dst->data_size);
#else
    if (dst == src) {
        return memory;
    }

    result = flecs_balloc(dst);
    if (result && src) {
        ecs_size_t size = src->data_size;
        if (dst->data_size < size) {
            size = dst->data_size;
        }
        ecs_os_memcpy(result, memory, size);
    }
    flecs_bfree(src, memory);
#endif
#ifdef FLECS_MEMSET_UNINITIALIZED
    if (dst && src && (dst->data_size > src->data_size)) {
        ecs_os_memset(ECS_OFFSET(result, src->data_size), 0xAA, 
            dst->data_size - src->data_size);
    } else if (dst && !src) {
        ecs_os_memset(result, 0xAA, dst->data_size);
    }
#endif

    return result;
}

void* flecs_bdup(
    ecs_block_allocator_t *ba, 
    void *memory)
{
#ifdef FLECS_USE_OS_ALLOC
    if (memory && ba->chunk_size) {
        return ecs_os_memdup(memory, ba->data_size);
    } else {
        return NULL;
    }
#endif

    void *result = flecs_balloc(ba);
    if (result) {
        ecs_os_memcpy(result, memory, ba->data_size);
    }
    return result;
}

/**
 * @file datastructures/hashmap.c
 * @brief Hashmap data structure.
 * 
 * The hashmap data structure is built on top of the map data structure. Where 
 * the map data structure can only work with 64bit key values, the hashmap can
 * hash keys of any size, and handles collisions between hashes.
 */


static
int32_t flecs_hashmap_find_key(
    const ecs_hashmap_t *map,
    ecs_vec_t *keys,
    ecs_size_t key_size, 
    const void *key)
{
    int32_t i, count = ecs_vec_count(keys);
    void *key_array = ecs_vec_first(keys);
    for (i = 0; i < count; i ++) {
        void *key_ptr = ECS_OFFSET(key_array, key_size * i);
        if (map->compare(key_ptr, key) == 0) {
            return i;
        }
    }
    return -1;
}

void _flecs_hashmap_init(
    ecs_hashmap_t *map,
    ecs_size_t key_size,
    ecs_size_t value_size,
    ecs_hash_value_action_t hash,
    ecs_compare_action_t compare,
    ecs_allocator_t *allocator)
{
    map->key_size = key_size;
    map->value_size = value_size;
    map->hash = hash;
    map->compare = compare;
    flecs_ballocator_init_t(&map->bucket_allocator, ecs_hm_bucket_t);
    ecs_map_init(&map->impl, allocator);
}

void flecs_hashmap_fini(
    ecs_hashmap_t *map)
{
    ecs_allocator_t *a = map->impl.allocator;
    ecs_map_iter_t it = ecs_map_iter(&map->impl);

    while (ecs_map_next(&it)) {
        ecs_hm_bucket_t *bucket = ecs_map_ptr(&it);
        ecs_vec_fini(a, &bucket->keys, map->key_size);
        ecs_vec_fini(a, &bucket->values, map->value_size);
#ifdef FLECS_SANITIZE        
        flecs_bfree(&map->bucket_allocator, bucket);
#endif
    }

    flecs_ballocator_fini(&map->bucket_allocator);
    ecs_map_fini(&map->impl);
}

void flecs_hashmap_copy(
    ecs_hashmap_t *dst,
    const ecs_hashmap_t *src)
{
    ecs_assert(dst != src, ECS_INVALID_PARAMETER, NULL);

    _flecs_hashmap_init(dst, src->key_size, src->value_size, src->hash, 
        src->compare, src->impl.allocator);
    ecs_map_copy(&dst->impl, &src->impl);

    ecs_allocator_t *a = dst->impl.allocator;
    ecs_map_iter_t it = ecs_map_iter(&dst->impl);
    while (ecs_map_next(&it)) {
        ecs_hm_bucket_t **bucket_ptr = ecs_map_ref(&it, ecs_hm_bucket_t);
        ecs_hm_bucket_t *src_bucket = bucket_ptr[0];
        ecs_hm_bucket_t *dst_bucket = flecs_balloc(&dst->bucket_allocator);
        bucket_ptr[0] = dst_bucket;
        dst_bucket->keys = ecs_vec_copy(a, &src_bucket->keys, dst->key_size);
        dst_bucket->values = ecs_vec_copy(a, &src_bucket->values, dst->value_size);
    }
}

void* _flecs_hashmap_get(
    const ecs_hashmap_t *map,
    ecs_size_t key_size,
    const void *key,
    ecs_size_t value_size)
{
    ecs_assert(map->key_size == key_size, ECS_INVALID_PARAMETER, NULL);
    ecs_assert(map->value_size == value_size, ECS_INVALID_PARAMETER, NULL);

    uint64_t hash = map->hash(key);
    ecs_hm_bucket_t *bucket = ecs_map_get_deref(&map->impl, 
        ecs_hm_bucket_t, hash);
    if (!bucket) {
        return NULL;
    }

    int32_t index = flecs_hashmap_find_key(map, &bucket->keys, key_size, key);
    if (index == -1) {
        return NULL;
    }

    return ecs_vec_get(&bucket->values, value_size, index);
}

flecs_hashmap_result_t _flecs_hashmap_ensure(
    ecs_hashmap_t *map,
    ecs_size_t key_size,
    const void *key,
    ecs_size_t value_size)
{
    ecs_assert(map->key_size == key_size, ECS_INVALID_PARAMETER, NULL);
    ecs_assert(map->value_size == value_size, ECS_INVALID_PARAMETER, NULL);

    uint64_t hash = map->hash(key);
    ecs_hm_bucket_t **r = ecs_map_ensure_ref(&map->impl, ecs_hm_bucket_t, hash);
    ecs_hm_bucket_t *bucket = r[0];
    if (!bucket) {
        bucket = r[0] = flecs_bcalloc(&map->bucket_allocator);
    }

    ecs_allocator_t *a = map->impl.allocator;
    void *value_ptr, *key_ptr;
    ecs_vec_t *keys = &bucket->keys;
    ecs_vec_t *values = &bucket->values;
    if (!keys->array) {
        keys = ecs_vec_init(a, &bucket->keys, key_size, 1);
        values = ecs_vec_init(a, &bucket->values, value_size, 1);
        key_ptr = ecs_vec_append(a, keys, key_size);        
        value_ptr = ecs_vec_append(a, values, value_size);
        ecs_os_memcpy(key_ptr, key, key_size);
        ecs_os_memset(value_ptr, 0, value_size);
    } else {
        int32_t index = flecs_hashmap_find_key(map, keys, key_size, key);
        if (index == -1) {
            key_ptr = ecs_vec_append(a, keys, key_size);        
            value_ptr = ecs_vec_append(a, values, value_size);
            ecs_os_memcpy(key_ptr, key, key_size);
            ecs_os_memset(value_ptr, 0, value_size);
        } else {
            key_ptr = ecs_vec_get(keys, key_size, index);
            value_ptr = ecs_vec_get(values, value_size, index);
        }
    }

    return (flecs_hashmap_result_t){
        .key = key_ptr, .value = value_ptr, .hash = hash
    };
}

void _flecs_hashmap_set(
    ecs_hashmap_t *map,
    ecs_size_t key_size,
    void *key,
    ecs_size_t value_size,
    const void *value)
{
    void *value_ptr = _flecs_hashmap_ensure(map, key_size, key, value_size).value;
    ecs_assert(value_ptr != NULL, ECS_INTERNAL_ERROR, NULL);
    ecs_os_memcpy(value_ptr, value, value_size);
}

ecs_hm_bucket_t* flecs_hashmap_get_bucket(
    const ecs_hashmap_t *map,
    uint64_t hash)
{
    ecs_assert(map != NULL, ECS_INTERNAL_ERROR, NULL);
    return ecs_map_get_deref(&map->impl, ecs_hm_bucket_t, hash);
}

void flecs_hm_bucket_remove(
    ecs_hashmap_t *map,
    ecs_hm_bucket_t *bucket,
    uint64_t hash,
    int32_t index)
{
    ecs_vec_remove(&bucket->keys, map->key_size, index);
    ecs_vec_remove(&bucket->values, map->value_size, index);

    if (!ecs_vec_count(&bucket->keys)) {
        ecs_allocator_t *a = map->impl.allocator;
        ecs_vec_fini(a, &bucket->keys, map->key_size);
        ecs_vec_fini(a, &bucket->values, map->value_size);
        ecs_hm_bucket_t *b = ecs_map_remove_ptr(&map->impl, hash);
        ecs_assert(bucket == b, ECS_INTERNAL_ERROR, NULL); (void)b;
        flecs_bfree(&map->bucket_allocator, bucket);
    }
}

void _flecs_hashmap_remove_w_hash(
    ecs_hashmap_t *map,
    ecs_size_t key_size,
    const void *key,
    ecs_size_t value_size,
    uint64_t hash)
{
    ecs_assert(map->key_size == key_size, ECS_INVALID_PARAMETER, NULL);
    ecs_assert(map->value_size == value_size, ECS_INVALID_PARAMETER, NULL);
    (void)value_size;

    ecs_hm_bucket_t *bucket = ecs_map_get_deref(&map->impl, 
        ecs_hm_bucket_t, hash);
    if (!bucket) {
        return;
    }

    int32_t index = flecs_hashmap_find_key(map, &bucket->keys, key_size, key);
    if (index == -1) {
        return;
    }

    flecs_hm_bucket_remove(map, bucket, hash, index);
}

void _flecs_hashmap_remove(
    ecs_hashmap_t *map,
    ecs_size_t key_size,
    const void *key,
    ecs_size_t value_size)
{
    ecs_assert(map->key_size == key_size, ECS_INVALID_PARAMETER, NULL);
    ecs_assert(map->value_size == value_size, ECS_INVALID_PARAMETER, NULL);

    uint64_t hash = map->hash(key);
    _flecs_hashmap_remove_w_hash(map, key_size, key, value_size, hash);
}

flecs_hashmap_iter_t flecs_hashmap_iter(
    ecs_hashmap_t *map)
{
    return (flecs_hashmap_iter_t){
        .it = ecs_map_iter(&map->impl)
    };
}

void* _flecs_hashmap_next(
    flecs_hashmap_iter_t *it,
    ecs_size_t key_size,
    void *key_out,
    ecs_size_t value_size)
{
    int32_t index = ++ it->index;
    ecs_hm_bucket_t *bucket = it->bucket;
    while (!bucket || it->index >= ecs_vec_count(&bucket->keys)) {
        ecs_map_next(&it->it);
        bucket = it->bucket = ecs_map_ptr(&it->it);
        if (!bucket) {
            return NULL;
        }
        index = it->index = 0;
    }

    if (key_out) {
        *(void**)key_out = ecs_vec_get(&bucket->keys, key_size, index);
    }
    
    return ecs_vec_get(&bucket->values, value_size, index);
}

/**
 * @file datastructures/stack_allocator.c
 * @brief Stack allocator.
 * 
 * The stack allocator enables pushing and popping values to a stack, and has
 * a lower overhead when compared to block allocators. A stack allocator is a
 * good fit for small temporary allocations.
 * 
 * The stack allocator allocates memory in pages. If the requested size of an
 * allocation exceeds the page size, a regular allocator is used instead.
 */


#define FLECS_STACK_PAGE_OFFSET ECS_ALIGN(ECS_SIZEOF(ecs_stack_page_t), 16)

int64_t ecs_stack_allocator_alloc_count = 0;
int64_t ecs_stack_allocator_free_count = 0;

static
ecs_stack_page_t* flecs_stack_page_new(uint32_t page_id) {
    ecs_stack_page_t *result = ecs_os_malloc(
        FLECS_STACK_PAGE_OFFSET + ECS_STACK_PAGE_SIZE);
    result->data = ECS_OFFSET(result, FLECS_STACK_PAGE_OFFSET);
    result->next = NULL;
    result->id = page_id + 1;
    ecs_os_linc(&ecs_stack_allocator_alloc_count);
    return result;
}

void* flecs_stack_alloc(
    ecs_stack_t *stack, 
    ecs_size_t size,
    ecs_size_t align)
{
    ecs_stack_page_t *page = stack->cur;
    if (page == &stack->first && !page->data) {
        page->data = ecs_os_malloc(ECS_STACK_PAGE_SIZE);
        ecs_os_linc(&ecs_stack_allocator_alloc_count);
    }

    int16_t sp = flecs_ito(int16_t, ECS_ALIGN(page->sp, align));
    int16_t next_sp = flecs_ito(int16_t, sp + size);
    void *result = NULL;

    if (next_sp > ECS_STACK_PAGE_SIZE) {
        if (size > ECS_STACK_PAGE_SIZE) {
            result = ecs_os_malloc(size); /* Too large for page */
            goto done;
        }

        if (page->next) {
            page = page->next;
        } else {
            page = page->next = flecs_stack_page_new(page->id);
        }
        sp = 0;
        next_sp = flecs_ito(int16_t, size);
        stack->cur = page;
    }

    page->sp = next_sp;
    result = ECS_OFFSET(page->data, sp);

done:
#ifdef FLECS_SANITIZE
    ecs_os_memset(result, 0xAA, size);
#endif
    return result;
}

void* flecs_stack_calloc(
    ecs_stack_t *stack, 
    ecs_size_t size,
    ecs_size_t align)
{
    void *ptr = flecs_stack_alloc(stack, size, align);
    ecs_os_memset(ptr, 0, size);
    return ptr;
}

void flecs_stack_free(
    void *ptr,
    ecs_size_t size)
{
    if (size > ECS_STACK_PAGE_SIZE) {
        ecs_os_free(ptr);
    }
}

ecs_stack_cursor_t flecs_stack_get_cursor(
    ecs_stack_t *stack)
{
    return (ecs_stack_cursor_t){
        .cur = stack->cur, .sp = stack->cur->sp
    };
}

void flecs_stack_restore_cursor(
    ecs_stack_t *stack,
    const ecs_stack_cursor_t *cursor)
{
    ecs_stack_page_t *cur = cursor->cur;
    if (!cur) {
        return;
    }

    if (cur == stack->cur) {
        if (cursor->sp > stack->cur->sp) {
            return;
        }
    } else if (cur->id > stack->cur->id) {
        return;
    }

    stack->cur = cursor->cur;
    stack->cur->sp = cursor->sp;
}

void flecs_stack_reset(
    ecs_stack_t *stack)
{
    stack->cur = &stack->first;
    stack->first.sp = 0;
}

void flecs_stack_init(
    ecs_stack_t *stack)
{
    ecs_os_zeromem(stack);
    stack->cur = &stack->first;
    stack->first.data = NULL;
}

void flecs_stack_fini(
    ecs_stack_t *stack)
{
    ecs_stack_page_t *next, *cur = &stack->first;
    ecs_assert(stack->cur == &stack->first, ECS_LEAK_DETECTED, NULL);
    ecs_assert(stack->cur->sp == 0, ECS_LEAK_DETECTED, NULL);
    do {
        next = cur->next;
        if (cur == &stack->first) {
            if (cur->data) {
                ecs_os_linc(&ecs_stack_allocator_free_count);
            }
            ecs_os_free(cur->data);
        } else {
            ecs_os_linc(&ecs_stack_allocator_free_count);
            ecs_os_free(cur);
        }
    } while ((cur = next));
}

/**
 * @file entity_filter.c
 * @brief Filters that are applied to entities in a table.
 * 
 * After a table has been matched by a query, additional filters may have to
 * be applied before returning entities to the application. The two scenarios
 * under which this happens are queries for union relationship pairs (entities
 * for multiple targets are stored in the same table) and toggles (components 
 * that are enabled/disabled with a bitset).
 */


static
int flecs_entity_filter_find_smallest_term(
    ecs_table_t *table,
    ecs_entity_filter_iter_t *iter)
{
    ecs_assert(table->_ != NULL, ECS_INTERNAL_ERROR, NULL);
    flecs_switch_term_t *sw_terms = ecs_vec_first(&iter->entity_filter->sw_terms);
    int32_t i, count = ecs_vec_count(&iter->entity_filter->sw_terms);
    int32_t min = INT_MAX, index = 0;

    for (i = 0; i < count; i ++) {
        /* The array with sparse queries for the matched table */
        flecs_switch_term_t *sparse_column = &sw_terms[i];

        /* Pointer to the switch column struct of the table */
        ecs_switch_t *sw = sparse_column->sw_column;

        /* If the sparse column pointer hadn't been retrieved yet, do it now */
        if (!sw) {
            /* Get the table column index from the signature column index */
            int32_t table_column_index = iter->columns[
                sparse_column->signature_column_index];

            /* Translate the table column index to switch column index */
            table_column_index -= table->_->sw_offset;
            ecs_assert(table_column_index >= 1, ECS_INTERNAL_ERROR, NULL);

            /* Get the sparse column */
            sw = sparse_column->sw_column = 
                &table->_->sw_columns[table_column_index - 1];
        }

        /* Find the smallest column */
        int32_t case_count = flecs_switch_case_count(sw, sparse_column->sw_case);
        if (case_count < min) {
            min = case_count;
            index = i + 1;
        }
    }

    return index;
}

static
int flecs_entity_filter_switch_next(
    ecs_table_t *table,
    ecs_entity_filter_iter_t *iter,
    bool filter)
{
    bool first_iteration = false;
    int32_t switch_smallest;

    if (!(switch_smallest = iter->sw_smallest)) {
        switch_smallest = iter->sw_smallest = 
            flecs_entity_filter_find_smallest_term(table, iter);
        first_iteration = true;
    }

    switch_smallest -= 1;

    flecs_switch_term_t *columns = ecs_vec_first(&iter->entity_filter->sw_terms);
    flecs_switch_term_t *column = &columns[switch_smallest];
    ecs_switch_t *sw, *sw_smallest = column->sw_column;
    ecs_entity_t case_smallest = column->sw_case;

    /* Find next entity to iterate in sparse column */
    int32_t first, sparse_first = iter->sw_offset;

    if (!filter) {
        if (first_iteration) {
            first = flecs_switch_first(sw_smallest, case_smallest);
        } else {
            first = flecs_switch_next(sw_smallest, sparse_first);
        }
    } else {
        int32_t cur_first = iter->range.offset, cur_count = iter->range.count;
        first = cur_first;
        while (flecs_switch_get(sw_smallest, first) != case_smallest) {
            first ++;
            if (first >= (cur_first + cur_count)) {
                first = -1;
                break;
            }
        }
    }

    if (first == -1) {
        goto done;
    }

    /* Check if entity matches with other sparse columns, if any */
    int32_t i, count = ecs_vec_count(&iter->entity_filter->sw_terms);
    do {
        for (i = 0; i < count; i ++) {
            if (i == switch_smallest) {
                /* Already validated this one */
                continue;
            }

            column = &columns[i];
            sw = column->sw_column;

            if (flecs_switch_get(sw, first) != column->sw_case) {
                first = flecs_switch_next(sw_smallest, first);
                if (first == -1) {
                    goto done;
                }
            }
        }
    } while (i != count);

    iter->range.offset = iter->sw_offset = first;
    iter->range.count = 1;

    return 0;
done:
    /* Iterated all elements in the sparse list, we should move to the
     * next matched table. */
    iter->sw_smallest = 0;
    iter->sw_offset = 0;

    return -1;
}

#define BS_MAX ((uint64_t)0xFFFFFFFFFFFFFFFF)

static
int flecs_entity_filter_bitset_next(
    ecs_table_t *table,
    ecs_entity_filter_iter_t *iter)
{
    /* Precomputed single-bit test */
    static const uint64_t bitmask[64] = {
    (uint64_t)1 << 0, (uint64_t)1 << 1, (uint64_t)1 << 2, (uint64_t)1 << 3,
    (uint64_t)1 << 4, (uint64_t)1 << 5, (uint64_t)1 << 6, (uint64_t)1 << 7,
    (uint64_t)1 << 8, (uint64_t)1 << 9, (uint64_t)1 << 10, (uint64_t)1 << 11,
    (uint64_t)1 << 12, (uint64_t)1 << 13, (uint64_t)1 << 14, (uint64_t)1 << 15,
    (uint64_t)1 << 16, (uint64_t)1 << 17, (uint64_t)1 << 18, (uint64_t)1 << 19,
    (uint64_t)1 << 20, (uint64_t)1 << 21, (uint64_t)1 << 22, (uint64_t)1 << 23,
    (uint64_t)1 << 24, (uint64_t)1 << 25, (uint64_t)1 << 26, (uint64_t)1 << 27,  
    (uint64_t)1 << 28, (uint64_t)1 << 29, (uint64_t)1 << 30, (uint64_t)1 << 31,
    (uint64_t)1 << 32, (uint64_t)1 << 33, (uint64_t)1 << 34, (uint64_t)1 << 35,  
    (uint64_t)1 << 36, (uint64_t)1 << 37, (uint64_t)1 << 38, (uint64_t)1 << 39,
    (uint64_t)1 << 40, (uint64_t)1 << 41, (uint64_t)1 << 42, (uint64_t)1 << 43,
    (uint64_t)1 << 44, (uint64_t)1 << 45, (uint64_t)1 << 46, (uint64_t)1 << 47,  
    (uint64_t)1 << 48, (uint64_t)1 << 49, (uint64_t)1 << 50, (uint64_t)1 << 51,
    (uint64_t)1 << 52, (uint64_t)1 << 53, (uint64_t)1 << 54, (uint64_t)1 << 55,  
    (uint64_t)1 << 56, (uint64_t)1 << 57, (uint64_t)1 << 58, (uint64_t)1 << 59,
    (uint64_t)1 << 60, (uint64_t)1 << 61, (uint64_t)1 << 62, (uint64_t)1 << 63
    };

    /* Precomputed test to verify if remainder of block is set (or not) */
    static const uint64_t bitmask_remain[64] = {
    BS_MAX, BS_MAX - (BS_MAX >> 63), BS_MAX - (BS_MAX >> 62),
    BS_MAX - (BS_MAX >> 61), BS_MAX - (BS_MAX >> 60), BS_MAX - (BS_MAX >> 59),
    BS_MAX - (BS_MAX >> 58), BS_MAX - (BS_MAX >> 57), BS_MAX - (BS_MAX >> 56), 
    BS_MAX - (BS_MAX >> 55), BS_MAX - (BS_MAX >> 54), BS_MAX - (BS_MAX >> 53), 
    BS_MAX - (BS_MAX >> 52), BS_MAX - (BS_MAX >> 51), BS_MAX - (BS_MAX >> 50), 
    BS_MAX - (BS_MAX >> 49), BS_MAX - (BS_MAX >> 48), BS_MAX - (BS_MAX >> 47), 
    BS_MAX - (BS_MAX >> 46), BS_MAX - (BS_MAX >> 45), BS_MAX - (BS_MAX >> 44), 
    BS_MAX - (BS_MAX >> 43), BS_MAX - (BS_MAX >> 42), BS_MAX - (BS_MAX >> 41), 
    BS_MAX - (BS_MAX >> 40), BS_MAX - (BS_MAX >> 39), BS_MAX - (BS_MAX >> 38), 
    BS_MAX - (BS_MAX >> 37), BS_MAX - (BS_MAX >> 36), BS_MAX - (BS_MAX >> 35), 
    BS_MAX - (BS_MAX >> 34), BS_MAX - (BS_MAX >> 33), BS_MAX - (BS_MAX >> 32), 
    BS_MAX - (BS_MAX >> 31), BS_MAX - (BS_MAX >> 30), BS_MAX - (BS_MAX >> 29), 
    BS_MAX - (BS_MAX >> 28), BS_MAX - (BS_MAX >> 27), BS_MAX - (BS_MAX >> 26), 
    BS_MAX - (BS_MAX >> 25), BS_MAX - (BS_MAX >> 24), BS_MAX - (BS_MAX >> 23), 
    BS_MAX - (BS_MAX >> 22), BS_MAX - (BS_MAX >> 21), BS_MAX - (BS_MAX >> 20), 
    BS_MAX - (BS_MAX >> 19), BS_MAX - (BS_MAX >> 18), BS_MAX - (BS_MAX >> 17), 
    BS_MAX - (BS_MAX >> 16), BS_MAX - (BS_MAX >> 15), BS_MAX - (BS_MAX >> 14), 
    BS_MAX - (BS_MAX >> 13), BS_MAX - (BS_MAX >> 12), BS_MAX - (BS_MAX >> 11), 
    BS_MAX - (BS_MAX >> 10), BS_MAX - (BS_MAX >> 9), BS_MAX - (BS_MAX >> 8), 
    BS_MAX - (BS_MAX >> 7), BS_MAX - (BS_MAX >> 6), BS_MAX - (BS_MAX >> 5), 
    BS_MAX - (BS_MAX >> 4), BS_MAX - (BS_MAX >> 3), BS_MAX - (BS_MAX >> 2),
    BS_MAX - (BS_MAX >> 1)
    };

    int32_t i, count = ecs_vec_count(&iter->entity_filter->bs_terms);
    flecs_bitset_term_t *terms = ecs_vec_first(&iter->entity_filter->bs_terms);
    int32_t bs_offset = table->_->bs_offset;
    int32_t first = iter->bs_offset;
    int32_t last = 0;

    for (i = 0; i < count; i ++) {
        flecs_bitset_term_t *column = &terms[i];
        ecs_bitset_t *bs = terms[i].bs_column;

        if (!bs) {
            int32_t index = column->column_index;
            ecs_assert((index - bs_offset >= 0), ECS_INTERNAL_ERROR, NULL);
            bs = &table->_->bs_columns[index - bs_offset];
            terms[i].bs_column = bs;
        }
        
        int32_t bs_elem_count = bs->count;
        int32_t bs_block = first >> 6;
        int32_t bs_block_count = ((bs_elem_count - 1) >> 6) + 1;

        if (bs_block >= bs_block_count) {
            goto done;
        }

        uint64_t *data = bs->data;
        int32_t bs_start = first & 0x3F;

        /* Step 1: find the first non-empty block */
        uint64_t v = data[bs_block];
        uint64_t remain = bitmask_remain[bs_start];
        while (!(v & remain)) {
            /* If no elements are remaining, move to next block */
            if ((++bs_block) >= bs_block_count) {
                /* No non-empty blocks left */
                goto done;
            }

            bs_start = 0;
            remain = BS_MAX; /* Test the full block */
            v = data[bs_block];
        }

        /* Step 2: find the first non-empty element in the block */
        while (!(v & bitmask[bs_start])) {
            bs_start ++;

            /* Block was not empty, so bs_start must be smaller than 64 */
            ecs_assert(bs_start < 64, ECS_INTERNAL_ERROR, NULL);
        }

        /* Step 3: Find number of contiguous enabled elements after start */
        int32_t bs_end = bs_start, bs_block_end = bs_block;

        remain = bitmask_remain[bs_end];
        while ((v & remain) == remain) {
            bs_end = 0;
            bs_block_end ++;

            if (bs_block_end == bs_block_count) {
                break;
            }

            v = data[bs_block_end];
            remain = BS_MAX; /* Test the full block */
        }

        /* Step 4: find remainder of enabled elements in current block */
        if (bs_block_end != bs_block_count) {
            while ((v & bitmask[bs_end])) {
                bs_end ++;
            }
        }

        /* Block was not 100% occupied, so bs_start must be smaller than 64 */
        ecs_assert(bs_end < 64, ECS_INTERNAL_ERROR, NULL);

        /* Step 5: translate to element start/end and make sure that each column
         * range is a subset of the previous one. */
        first = bs_block * 64 + bs_start;
        int32_t cur_last = bs_block_end * 64 + bs_end;
        
        /* No enabled elements found in table */
        if (first == cur_last) {
            goto done;
        }

        /* If multiple bitsets are evaluated, make sure each subsequent range
         * is equal or a subset of the previous range */
        if (i) {
            /* If the first element of a subsequent bitset is larger than the
             * previous last value, start over. */
            if (first >= last) {
                i = -1;
                continue;
            }

            /* Make sure the last element of the range doesn't exceed the last
             * element of the previous range. */
            if (cur_last > last) {
                cur_last = last;
            }
        }

        last = cur_last;
        int32_t elem_count = last - first;

        /* Make sure last element doesn't exceed total number of elements in 
         * the table */
        if (elem_count > (bs_elem_count - first)) {
            elem_count = (bs_elem_count - first);
            if (!elem_count) {
                iter->bs_offset = 0;
                goto done;
            }
        }
        
        iter->range.offset = first;
        iter->range.count = elem_count;
        iter->bs_offset = first;
    }

    /* Keep track of last processed element for iteration */ 
    iter->bs_offset = last;

    return 0;
done:
    iter->sw_smallest = 0;
    iter->sw_offset = 0;
    return -1;
}

#undef BS_MAX

static
int32_t flecs_get_flattened_target(
    ecs_world_t *world,
    EcsTarget *cur,
    ecs_entity_t rel,
    ecs_id_t id,
    ecs_entity_t *src_out,
    ecs_table_record_t **tr_out)
{
    ecs_id_record_t *idr = flecs_id_record_get(world, id);
    if (!idr) {
        return -1;
    }

    ecs_record_t *r = cur->target;
    ecs_assert(r != NULL, ECS_INTERNAL_ERROR, NULL);

    ecs_table_t *table = r->table;
    if (!table) {
        return -1;
    }

    const ecs_table_record_t *tr = flecs_id_record_get_table(idr, table);
    if (tr) {
        *src_out = ecs_record_get_entity(r);
        *tr_out = (ecs_table_record_t*)tr;
        return tr->column;
    }

    if (table->flags & EcsTableHasTarget) {
        int32_t col = table->storage_map[table->_->ft_offset];
        ecs_assert(col != -1, ECS_INTERNAL_ERROR, NULL);
        EcsTarget *next = table->data.columns[col].array;
        next = ECS_ELEM_T(next, EcsTarget, ECS_RECORD_TO_ROW(r->row));
        return flecs_get_flattened_target(
            world, next, rel, id, src_out, tr_out);
    }

    return ecs_search_relation(
        world, table, 0, id, rel, EcsSelf|EcsUp, src_out, NULL, tr_out);
}

void flecs_entity_filter_init(
    ecs_world_t *world,
    ecs_entity_filter_t **entity_filter,
    const ecs_filter_t *filter,
    const ecs_table_t *table,
    ecs_id_t *ids,
    int32_t *columns)
{
    ecs_poly_assert(world, ecs_world_t);
    ecs_assert(entity_filter != NULL, ECS_INTERNAL_ERROR, NULL);
    ecs_assert(filter != NULL, ECS_INTERNAL_ERROR, NULL);
    ecs_assert(table != NULL, ECS_INTERNAL_ERROR, NULL);
    ecs_assert(ids != NULL, ECS_INTERNAL_ERROR, NULL);
    ecs_assert(columns != NULL, ECS_INTERNAL_ERROR, NULL);
    ecs_allocator_t *a = &world->allocator;
    ecs_entity_filter_t ef;
    ecs_os_zeromem(&ef);
    ecs_vec_t *sw_terms = &ef.sw_terms;
    ecs_vec_t *bs_terms = &ef.bs_terms;
    ecs_vec_t *ft_terms = &ef.ft_terms;
    if (*entity_filter) {
        ef.sw_terms = (*entity_filter)->sw_terms;
        ef.bs_terms = (*entity_filter)->bs_terms;
        ef.ft_terms = (*entity_filter)->ft_terms;
    }
    ecs_vec_reset_t(a, sw_terms, flecs_switch_term_t);
    ecs_vec_reset_t(a, bs_terms, flecs_bitset_term_t);
    ecs_vec_reset_t(a, ft_terms, flecs_flat_table_term_t);
    ecs_term_t *terms = filter->terms;
    int32_t i, term_count = filter->term_count;
    bool has_filter = false;
    ef.flat_tree_column = -1;

    /* Look for union fields */
    if (table->flags & EcsTableHasUnion) {
        for (i = 0; i < term_count; i ++) {
            if (ecs_term_match_0(&terms[i])) {
                continue;
            }

            ecs_id_t id = terms[i].id;
            if (ECS_HAS_ID_FLAG(id, PAIR) && ECS_PAIR_SECOND(id) == EcsWildcard) {
                continue;
            }
            
            int32_t field = terms[i].field_index;
            int32_t column = columns[field];
            if (column <= 0) {
                continue;
            }

            ecs_id_t table_id = table->type.array[column - 1];
            if (ECS_PAIR_FIRST(table_id) != EcsUnion) {
                continue;
            }

            flecs_switch_term_t *el = ecs_vec_append_t(a, sw_terms, 
                flecs_switch_term_t);
            el->signature_column_index = field;
            el->sw_case = ECS_PAIR_SECOND(id);
            el->sw_column = NULL;
            ids[field] = id;
            has_filter = true;
        }
    }

    /* Look for disabled fields */
    if (table->flags & EcsTableHasToggle) {
        for (i = 0; i < term_count; i ++) {
            if (ecs_term_match_0(&terms[i])) {
                continue;
            }

            int32_t field = terms[i].field_index;
            ecs_id_t id = ids[field];
            ecs_id_t bs_id = ECS_TOGGLE | id;
            int32_t bs_index = ecs_search(world, table, bs_id, 0);

            if (bs_index != -1) {
                flecs_bitset_term_t *bc = ecs_vec_append_t(a, bs_terms, 
                    flecs_bitset_term_t);
                bc->column_index = bs_index;
                bc->bs_column = NULL;
                has_filter = true;
            }
        }
    }

    /* Look for flattened fields */
    if (table->flags & EcsTableHasTarget) {
        const ecs_table_record_t *tr = flecs_table_record_get(world, table, 
            ecs_pair_t(EcsTarget, EcsWildcard));
        ecs_assert(tr != NULL, ECS_INTERNAL_ERROR, NULL);
        int32_t column = tr->column;
        ecs_assert(column != -1, ECS_INTERNAL_ERROR, NULL);
        ecs_entity_t rel = ecs_pair_second(world, table->type.array[column]);

        for (i = 0; i < term_count; i ++) {
            if (ecs_term_match_0(&terms[i])) {
                continue;
            }

            if (terms[i].src.trav == rel) {
                ef.flat_tree_column = table->storage_map[column];
                ecs_assert(ef.flat_tree_column != -1, 
                    ECS_INTERNAL_ERROR, NULL);
                has_filter = true;
                
                flecs_flat_table_term_t *term = ecs_vec_append_t(
                    a, ft_terms, flecs_flat_table_term_t);
                term->field_index = terms[i].field_index;
                term->term = &terms[i];
                ecs_os_zeromem(&term->monitor);
            }
        }
    }

    if (has_filter) {
        *entity_filter = ecs_os_malloc_t(ecs_entity_filter_t);
        ecs_assert(*entity_filter != NULL, ECS_OUT_OF_MEMORY, NULL);
        **entity_filter = ef;
    }
}

void flecs_entity_filter_fini(
    ecs_world_t *world,
    ecs_entity_filter_t *ef)
{
    if (!ef) {
        return;
    }

    ecs_allocator_t *a = &world->allocator;

    flecs_flat_table_term_t *fields = ecs_vec_first(&ef->ft_terms);
    int32_t i, term_count = ecs_vec_count(&ef->ft_terms);
    for (i = 0; i < term_count; i ++) {
        ecs_vec_fini_t(NULL, &fields[i].monitor, flecs_flat_monitor_t);
    }

    ecs_vec_fini_t(a, &ef->sw_terms, flecs_switch_term_t);
    ecs_vec_fini_t(a, &ef->bs_terms, flecs_bitset_term_t);
    ecs_vec_fini_t(a, &ef->ft_terms, flecs_flat_table_term_t);
    ecs_os_free(ef);
}

int flecs_entity_filter_next(
    ecs_entity_filter_iter_t *it)
{
    ecs_table_t *table = it->range.table;
    flecs_switch_term_t *sw_terms = ecs_vec_first(&it->entity_filter->sw_terms);
    flecs_bitset_term_t *bs_terms = ecs_vec_first(&it->entity_filter->bs_terms);
    ecs_entity_filter_t *ef = it->entity_filter;
    int32_t flat_tree_column = ef->flat_tree_column;
    ecs_table_range_t *range = &it->range;
    int32_t range_end = range->offset + range->count;
    int result = EcsIterNext;
    bool found = false;

    do {
        found = false;

        if (bs_terms) {
            if (flecs_entity_filter_bitset_next(table, it) == -1) {
                /* No more enabled components for table */
                it->bs_offset = 0;
                break;
            } else {
                result = EcsIterYield;
                found = true;
            }
        }

        if (sw_terms) {
            if (flecs_entity_filter_switch_next(table, it, found) == -1) {
                /* No more elements in sparse column */
                if (found) {
                    /* Try again */
                    result = EcsIterNext;
                    found = false;
                } else {
                    /* Nothing found */
                    it->bs_offset = 0;
                    break;
                }
            } else {
                result = EcsIterYield;
                found = true;
                it->bs_offset = range->offset + range->count;
            }
        }

        if (flat_tree_column != -1) {
            bool first_for_table = it->prev != table;
            ecs_iter_t *iter = it->it;
            ecs_world_t *world = iter->real_world;
            EcsTarget *ft = table->data.columns[flat_tree_column].array;
            int32_t ft_offset;
            int32_t ft_count;

            if (first_for_table) {
                ft_offset = it->flat_tree_offset = range->offset;
                it->target_count = 1;
            } else {
                it->flat_tree_offset += ft[it->flat_tree_offset].count;
                ft_offset = it->flat_tree_offset;
                it->target_count ++;
            }

            ecs_assert(ft_offset < ecs_table_count(table), 
                ECS_INTERNAL_ERROR, NULL);

            EcsTarget *cur = &ft[ft_offset];
            ft_count = cur->count;
            bool is_last = (ft_offset + ft_count) >= range_end;

            int32_t i, field_count = ecs_vec_count(&ef->ft_terms);
            flecs_flat_table_term_t *fields = ecs_vec_first(&ef->ft_terms);
            for (i = 0; i < field_count; i ++) {
                flecs_flat_table_term_t *field = &fields[i];
                ecs_vec_init_if_t(&field->monitor, flecs_flat_monitor_t);
                int32_t field_index = field->field_index;
                ecs_id_t id = it->it->ids[field_index];
                ecs_id_t flat_pair = table->type.array[flat_tree_column];
                ecs_entity_t rel = ECS_PAIR_FIRST(flat_pair);
                ecs_entity_t tgt;
                ecs_table_record_t *tr;
                int32_t tgt_col = flecs_get_flattened_target(
                    world, cur, rel, id, &tgt, &tr);
                if (tgt_col != -1) {
                    iter->sources[field_index] = tgt;
                    iter->columns[field_index] = /* encode flattened field */
                        -(iter->field_count + tgt_col + 1);
                    ecs_assert(tr != NULL, ECS_INTERNAL_ERROR, NULL);

                    /* Keep track of maximum value encountered in target table
                     * dirty state so this doesn't have to be recomputed when
                     * synchronizing the query monitor. */
                    ecs_vec_set_min_count_zeromem_t(NULL, &field->monitor, 
                        flecs_flat_monitor_t, it->target_count);
                    ecs_table_t *tgt_table = tr->hdr.table;
                    int32_t *ds = flecs_table_get_dirty_state(world, tgt_table);
                    ecs_assert(ds != NULL, ECS_INTERNAL_ERROR, NULL);
                    ecs_vec_get_t(&field->monitor, flecs_flat_monitor_t, 
                        it->target_count - 1)->table_state = ds[tgt_col + 1];
                } else {
                    if (field->term->oper == EcsOptional) {
                        iter->columns[field_index] = 0;
                        iter->ptrs[field_index] = NULL;
                    } else {
                        it->prev = NULL;
                        break;
                    }
                }
            }
            if (i != field_count) {
                if (is_last) {
                    break;
                }
            } else {
                found = true;
                if ((ft_offset + ft_count) == range_end) {
                    result = EcsIterNextYield;
                } else {
                    result = EcsIterYield;
                }
            }

            range->offset = ft_offset;
            range->count = ft_count;
            it->prev = table;
        }
    } while (!found);

    it->prev = table;

    if (!found) {
        return EcsIterNext;
    } else {
        return result;
    }
}

/**
 * @file addons/log.c
 * @brief Log addon.
 */


#ifdef FLECS_LOG

#include <ctype.h>

void flecs_colorize_buf(
    char *msg,
    bool enable_colors,
    ecs_strbuf_t *buf)
{
    ecs_assert(msg != NULL, ECS_INTERNAL_ERROR, NULL);
    ecs_assert(buf != NULL, ECS_INTERNAL_ERROR, NULL);

    char *ptr, ch, prev = '\0';
    bool isNum = false;
    char isStr = '\0';
    bool isVar = false;
    bool overrideColor = false;
    bool autoColor = true;
    bool dontAppend = false;

    for (ptr = msg; (ch = *ptr); ptr++) {
        dontAppend = false;

        if (!overrideColor) {
            if (isNum && !isdigit(ch) && !isalpha(ch) && (ch != '.') && (ch != '%')) {
                if (enable_colors) ecs_strbuf_appendlit(buf, ECS_NORMAL);
                isNum = false;
            }
            if (isStr && (isStr == ch) && prev != '\\') {
                isStr = '\0';
            } else if (((ch == '\'') || (ch == '"')) && !isStr &&
                !isalpha(prev) && (prev != '\\'))
            {
                if (enable_colors) ecs_strbuf_appendlit(buf, ECS_CYAN);
                isStr = ch;
            }

            if ((isdigit(ch) || (ch == '%' && isdigit(prev)) ||
                (ch == '-' && isdigit(ptr[1]))) && !isNum && !isStr && !isVar &&
                 !isalpha(prev) && !isdigit(prev) && (prev != '_') &&
                 (prev != '.'))
            {
                if (enable_colors) ecs_strbuf_appendlit(buf, ECS_GREEN);
                isNum = true;
            }

            if (isVar && !isalpha(ch) && !isdigit(ch) && ch != '_') {
                if (enable_colors) ecs_strbuf_appendlit(buf, ECS_NORMAL);
                isVar = false;
            }

            if (!isStr && !isVar && ch == '$' && isalpha(ptr[1])) {
                if (enable_colors) ecs_strbuf_appendlit(buf, ECS_CYAN);
                isVar = true;
            }
        }

        if (!isVar && !isStr && !isNum && ch == '#' && ptr[1] == '[') {
            bool isColor = true;
            overrideColor = true;

            /* Custom colors */
            if (!ecs_os_strncmp(&ptr[2], "]", ecs_os_strlen("]"))) {
                autoColor = false;
            } else if (!ecs_os_strncmp(&ptr[2], "green]", ecs_os_strlen("green]"))) {
                if (enable_colors) ecs_strbuf_appendlit(buf, ECS_GREEN);
            } else if (!ecs_os_strncmp(&ptr[2], "red]", ecs_os_strlen("red]"))) {
                if (enable_colors) ecs_strbuf_appendlit(buf, ECS_RED);
            } else if (!ecs_os_strncmp(&ptr[2], "blue]", ecs_os_strlen("red]"))) {
                if (enable_colors) ecs_strbuf_appendlit(buf, ECS_BLUE);
            } else if (!ecs_os_strncmp(&ptr[2], "magenta]", ecs_os_strlen("magenta]"))) {
                if (enable_colors) ecs_strbuf_appendlit(buf, ECS_MAGENTA);
            } else if (!ecs_os_strncmp(&ptr[2], "cyan]", ecs_os_strlen("cyan]"))) {
                if (enable_colors) ecs_strbuf_appendlit(buf, ECS_CYAN);
            } else if (!ecs_os_strncmp(&ptr[2], "yellow]", ecs_os_strlen("yellow]"))) {
                if (enable_colors) ecs_strbuf_appendlit(buf, ECS_YELLOW);
            } else if (!ecs_os_strncmp(&ptr[2], "grey]", ecs_os_strlen("grey]"))) {
                if (enable_colors) ecs_strbuf_appendlit(buf, ECS_GREY);
            } else if (!ecs_os_strncmp(&ptr[2], "white]", ecs_os_strlen("white]"))) {
                if (enable_colors) ecs_strbuf_appendlit(buf, ECS_NORMAL);
            } else if (!ecs_os_strncmp(&ptr[2], "bold]", ecs_os_strlen("bold]"))) {
                if (enable_colors) ecs_strbuf_appendlit(buf, ECS_BOLD);
            } else if (!ecs_os_strncmp(&ptr[2], "normal]", ecs_os_strlen("normal]"))) {
                if (enable_colors) ecs_strbuf_appendlit(buf, ECS_NORMAL);
            } else if (!ecs_os_strncmp(&ptr[2], "reset]", ecs_os_strlen("reset]"))) {
                overrideColor = false;
                if (enable_colors) ecs_strbuf_appendlit(buf, ECS_NORMAL);
            } else {
                isColor = false;
                overrideColor = false;
            }

            if (isColor) {
                ptr += 2;
                while ((ch = *ptr) != ']') ptr ++;
                dontAppend = true;
            }
            if (!autoColor) {
                overrideColor = true;
            }
        }

        if (ch == '\n') {
            if (isNum || isStr || isVar || overrideColor) {
                if (enable_colors) ecs_strbuf_appendlit(buf, ECS_NORMAL);
                overrideColor = false;
                isNum = false;
                isStr = false;
                isVar = false;
            }
        }

        if (!dontAppend) {
            ecs_strbuf_appendstrn(buf, ptr, 1);
        }

        if (!overrideColor) {
            if (((ch == '\'') || (ch == '"')) && !isStr) {
                if (enable_colors) ecs_strbuf_appendlit(buf, ECS_NORMAL);
            }
        }

        prev = ch;
    }

    if (isNum || isStr || isVar || overrideColor) {
        if (enable_colors) ecs_strbuf_appendlit(buf, ECS_NORMAL);
    }
}

void _ecs_printv(
    int level,
    const char *file,
    int32_t line,
    const char *fmt,
    va_list args)
{
    (void)level;
    (void)line;

    ecs_strbuf_t msg_buf = ECS_STRBUF_INIT;

    /* Apply color. Even if we don't want color, we still need to call the
     * colorize function to get rid of the color tags (e.g. #[green]) */
    char *msg_nocolor = ecs_vasprintf(fmt, args);
    flecs_colorize_buf(msg_nocolor, 
        ecs_os_api.flags_ & EcsOsApiLogWithColors, &msg_buf);
    ecs_os_free(msg_nocolor);

    char *msg = ecs_strbuf_get(&msg_buf);

    if (msg) {
        ecs_os_api.log_(level, file, line, msg);
        ecs_os_free(msg);
    } else {
        ecs_os_api.log_(level, file, line, "");
    }
}

void _ecs_print(
    int level,
    const char *file,
    int32_t line,
    const char *fmt,
    ...)
{
    va_list args;
    va_start(args, fmt);
    _ecs_printv(level, file, line, fmt, args);
    va_end(args);    
}

void _ecs_logv(
    int level,
    const char *file,
    int32_t line,
    const char *fmt,
    va_list args)
{
    if (level > ecs_os_api.log_level_) {
        return;
    }

    _ecs_printv(level, file, line, fmt, args);
}

void _ecs_log(
    int level,
    const char *file,
    int32_t line,
    const char *fmt,
    ...)
{
    if (level > ecs_os_api.log_level_) {
        return;
    }

    va_list args;
    va_start(args, fmt);
    _ecs_printv(level, file, line, fmt, args);
    va_end(args);    
}


void _ecs_log_push(
    int32_t level) 
{
    if (level <= ecs_os_api.log_level_) {
        ecs_os_api.log_indent_ ++;
    }
}

void _ecs_log_pop(
    int32_t level)
{
    if (level <= ecs_os_api.log_level_) {
        ecs_os_api.log_indent_ --;
        ecs_assert(ecs_os_api.log_indent_ >= 0, ECS_INTERNAL_ERROR, NULL);
    }
}

void _ecs_parser_errorv(
    const char *name,
    const char *expr, 
    int64_t column_arg,
    const char *fmt,
    va_list args)
{
    if (column_arg > 65536) {
        /* Limit column size, which prevents the code from throwing up when the
         * function is called with (expr - ptr), and expr is NULL. */
        column_arg = 0;
    }
    int32_t column = flecs_itoi32(column_arg);

    if (ecs_os_api.log_level_ >= -2) {
        ecs_strbuf_t msg_buf = ECS_STRBUF_INIT;

        ecs_strbuf_vappend(&msg_buf, fmt, args);

        if (expr) {
            ecs_strbuf_appendch(&msg_buf, '\n');

            /* Find start of line by taking column and looking for the
             * last occurring newline */
            if (column != -1) {
                const char *ptr = &expr[column];
                while (ptr[0] != '\n' && ptr > expr) {
                    ptr --;
                }

                if (ptr == expr) {
                    /* ptr is already at start of line */
                } else {
                    column -= (int32_t)(ptr - expr + 1);
                    expr = ptr + 1;
                }
            }

            /* Strip newlines from current statement, if any */            
            char *newline_ptr = strchr(expr, '\n');
            if (newline_ptr) {
                /* Strip newline from expr */
                ecs_strbuf_appendstrn(&msg_buf, expr, 
                    (int32_t)(newline_ptr - expr));
            } else {
                ecs_strbuf_appendstr(&msg_buf, expr);
            }

            ecs_strbuf_appendch(&msg_buf, '\n');

            if (column != -1) {
                int32_t c;
                for (c = 0; c < column; c ++) {
                    ecs_strbuf_appendch(&msg_buf, ' ');
                }
                ecs_strbuf_appendch(&msg_buf, '^');
            }
        }

        char *msg = ecs_strbuf_get(&msg_buf);
        ecs_os_err(name, 0, msg);
        ecs_os_free(msg);
    }
}

void _ecs_parser_error(
    const char *name,
    const char *expr, 
    int64_t column,
    const char *fmt,
    ...)
{
    if (ecs_os_api.log_level_  >= -2) {
        va_list args;
        va_start(args, fmt);
        _ecs_parser_errorv(name, expr, column, fmt, args);
        va_end(args);
    }
}

void _ecs_abort(
    int32_t err,
    const char *file,
    int32_t line,
    const char *fmt,
    ...)
{
    if (fmt) {
        va_list args;
        va_start(args, fmt);
        char *msg = ecs_vasprintf(fmt, args);
        va_end(args);
        _ecs_fatal(file, line, "%s (%s)", msg, ecs_strerror(err));
        ecs_os_free(msg);
    } else {
        _ecs_fatal(file, line, "%s", ecs_strerror(err));
    }
    ecs_os_api.log_last_error_ = err;
}

bool _ecs_assert(
    bool condition,
    int32_t err,
    const char *cond_str,
    const char *file,
    int32_t line,
    const char *fmt,
    ...)
{
    if (!condition) {
        if (fmt) {
            va_list args;
            va_start(args, fmt);
            char *msg = ecs_vasprintf(fmt, args);
            va_end(args);            
            _ecs_fatal(file, line, "assert: %s %s (%s)", 
                cond_str, msg, ecs_strerror(err));
            ecs_os_free(msg);
        } else {
            _ecs_fatal(file, line, "assert: %s %s", 
                cond_str, ecs_strerror(err));
        }
        ecs_os_api.log_last_error_ = err;
    }

    return condition;
}

void _ecs_deprecated(
    const char *file,
    int32_t line,
    const char *msg)
{
    _ecs_err(file, line, "%s", msg);
}

bool ecs_should_log(int32_t level) {
#   if !defined(FLECS_LOG_3)
    if (level == 3) {
        return false;
    }
#   endif
#   if !defined(FLECS_LOG_2)
    if (level == 2) {
        return false;
    }
#   endif
#   if !defined(FLECS_LOG_1)
    if (level == 1) {
        return false;
    }
#   endif

    return level <= ecs_os_api.log_level_;
}

#define ECS_ERR_STR(code) case code: return &(#code[4])

const char* ecs_strerror(
    int32_t error_code)
{
    switch (error_code) {
    ECS_ERR_STR(ECS_INVALID_PARAMETER);
    ECS_ERR_STR(ECS_NOT_A_COMPONENT);
    ECS_ERR_STR(ECS_INTERNAL_ERROR);
    ECS_ERR_STR(ECS_ALREADY_DEFINED);
    ECS_ERR_STR(ECS_INVALID_COMPONENT_SIZE);
    ECS_ERR_STR(ECS_INVALID_COMPONENT_ALIGNMENT);
    ECS_ERR_STR(ECS_NAME_IN_USE);
    ECS_ERR_STR(ECS_OUT_OF_MEMORY);
    ECS_ERR_STR(ECS_OPERATION_FAILED);
    ECS_ERR_STR(ECS_INVALID_CONVERSION);
    ECS_ERR_STR(ECS_MODULE_UNDEFINED);
    ECS_ERR_STR(ECS_MISSING_SYMBOL);
    ECS_ERR_STR(ECS_ALREADY_IN_USE);
    ECS_ERR_STR(ECS_CYCLE_DETECTED);
    ECS_ERR_STR(ECS_LEAK_DETECTED);
    ECS_ERR_STR(ECS_COLUMN_INDEX_OUT_OF_RANGE);
    ECS_ERR_STR(ECS_COLUMN_IS_NOT_SHARED);
    ECS_ERR_STR(ECS_COLUMN_IS_SHARED);
    ECS_ERR_STR(ECS_COLUMN_TYPE_MISMATCH);
    ECS_ERR_STR(ECS_INVALID_WHILE_READONLY);
    ECS_ERR_STR(ECS_INVALID_FROM_WORKER);
    ECS_ERR_STR(ECS_OUT_OF_RANGE);
    ECS_ERR_STR(ECS_MISSING_OS_API);
    ECS_ERR_STR(ECS_UNSUPPORTED);
    ECS_ERR_STR(ECS_ACCESS_VIOLATION);
    ECS_ERR_STR(ECS_COMPONENT_NOT_REGISTERED);
    ECS_ERR_STR(ECS_INCONSISTENT_COMPONENT_ID);
    ECS_ERR_STR(ECS_INCONSISTENT_NAME);
    ECS_ERR_STR(ECS_INCONSISTENT_COMPONENT_ACTION);
    ECS_ERR_STR(ECS_INVALID_OPERATION);
    ECS_ERR_STR(ECS_CONSTRAINT_VIOLATED);
    ECS_ERR_STR(ECS_LOCKED_STORAGE);
    ECS_ERR_STR(ECS_ID_IN_USE);
    }

    return "unknown error code";
}

#else

/* Empty bodies for when logging is disabled */

void _ecs_log(
    int32_t level,
    const char *file,
    int32_t line,
    const char *fmt,
    ...)
{
    (void)level;
    (void)file;
    (void)line;
    (void)fmt;
}

void _ecs_parser_error(
    const char *name,
    const char *expr, 
    int64_t column,
    const char *fmt,
    ...)
{
    (void)name;
    (void)expr;
    (void)column;
    (void)fmt;
}

void _ecs_parser_errorv(
    const char *name,
    const char *expr, 
    int64_t column,
    const char *fmt,
    va_list args)
{
    (void)name;
    (void)expr;
    (void)column;
    (void)fmt;
    (void)args;
}

void _ecs_abort(
    int32_t error_code,
    const char *file,
    int32_t line,
    const char *fmt,
    ...)
{
    (void)error_code;
    (void)file;
    (void)line;
    (void)fmt;
}

bool _ecs_assert(
    bool condition,
    int32_t error_code,
    const char *condition_str,
    const char *file,
    int32_t line,
    const char *fmt,
    ...)
{
    (void)condition;
    (void)error_code;
    (void)condition_str;
    (void)file;
    (void)line;
    (void)fmt;
    return true;
}

#endif

int ecs_log_get_level(void) {
    return ecs_os_api.log_level_;
}

int ecs_log_set_level(
    int level)
{
    int prev = level;
    ecs_os_api.log_level_ = level;
    return prev;
}

bool ecs_log_enable_colors(
    bool enabled)
{
    bool prev = ecs_os_api.flags_ & EcsOsApiLogWithColors;
    ECS_BIT_COND(ecs_os_api.flags_, EcsOsApiLogWithColors, enabled);
    return prev;
}

bool ecs_log_enable_timestamp(
    bool enabled)
{
    bool prev = ecs_os_api.flags_ & EcsOsApiLogWithTimeStamp;
    ECS_BIT_COND(ecs_os_api.flags_, EcsOsApiLogWithTimeStamp, enabled);
    return prev;
}

bool ecs_log_enable_timedelta(
    bool enabled)
{
    bool prev = ecs_os_api.flags_ & EcsOsApiLogWithTimeDelta;
    ECS_BIT_COND(ecs_os_api.flags_, EcsOsApiLogWithTimeDelta, enabled);
    return prev;
}

int ecs_log_last_error(void)
{
    int result = ecs_os_api.log_last_error_;
    ecs_os_api.log_last_error_ = 0;
    return result;
}

/**
 * @file addons/pipeline/worker.c
 * @brief Functions for running pipelines on one or more threads.
 */

/**
 * @file addons/system/system.c
 * @brief Internal types and functions for system addon.
 */

#ifndef FLECS_SYSTEM_PRIVATE_H
#define FLECS_SYSTEM_PRIVATE_H

#ifdef FLECS_SYSTEM


#define ecs_system_t_magic     (0x65637383)
#define ecs_system_t_tag       EcsSystem

extern ecs_mixins_t ecs_system_t_mixins;

typedef struct ecs_system_t {
    ecs_header_t hdr;

    ecs_run_action_t run;           /* See ecs_system_desc_t */
    ecs_iter_action_t action;       /* See ecs_system_desc_t */

    ecs_query_t *query;             /* System query */
    ecs_entity_t query_entity;      /* Entity associated with query */
    ecs_entity_t tick_source;       /* Tick source associated with system */
    
    /* Schedule parameters */
    bool multi_threaded;
    bool no_readonly;

    int64_t invoke_count;           /* Number of times system is invoked */
    ecs_ftime_t time_spent;         /* Time spent on running system */
    ecs_ftime_t time_passed;        /* Time passed since last invocation */
    int64_t last_frame;             /* Last frame for which the system was considered */

    void *ctx;                      /* Userdata for system */
    void *binding_ctx;              /* Optional language binding context */

    ecs_ctx_free_t ctx_free;
    ecs_ctx_free_t binding_ctx_free;

    /* Mixins */
    ecs_world_t *world;
    ecs_entity_t entity;
    ecs_poly_dtor_t dtor;      
} ecs_system_t;

/* Invoked when system becomes active / inactive */
void ecs_system_activate(
    ecs_world_t *world,
    ecs_entity_t system,
    bool activate,
    const ecs_system_t *system_data);

/* Internal function to run a system */
ecs_entity_t ecs_run_intern(
    ecs_world_t *world,
    ecs_stage_t *stage,
    ecs_entity_t system,
    ecs_system_t *system_data,
    int32_t stage_current,
    int32_t stage_count,
    ecs_ftime_t delta_time,
    int32_t offset,
    int32_t limit,
    void *param);

#endif

#endif


#ifdef FLECS_PIPELINE
/**
 * @file addons/pipeline/pipeline.h
 * @brief Internal functions/types for pipeline addon.
 */

#ifndef FLECS_PIPELINE_PRIVATE_H
#define FLECS_PIPELINE_PRIVATE_H


/** Instruction data for pipeline.
 * This type is the element type in the "ops" vector of a pipeline. */
typedef struct ecs_pipeline_op_t {
    int32_t offset;             /* Offset in systems vector */
    int32_t count;              /* Number of systems to run before next op */
    bool multi_threaded;        /* Whether systems can be ran multi threaded */
    bool no_readonly;           /* Whether systems are staged or not */
} ecs_pipeline_op_t;

typedef struct ecs_pipeline_state_t {
    ecs_query_t *query;         /* Pipeline query */
    ecs_vec_t ops;              /* Pipeline schedule */
    ecs_vec_t systems;          /* Vector with system ids */


    ecs_entity_t last_system;   /* Last system ran by pipeline */
    ecs_id_record_t *idr_inactive; /* Cached record for quick inactive test */
    int32_t match_count;        /* Used to track of rebuild is necessary */
    int32_t rebuild_count;      /* Number of pipeline rebuilds */
    ecs_iter_t *iters;          /* Iterator for worker(s) */
    int32_t iter_count;

    /* Members for continuing pipeline iteration after pipeline rebuild */
    ecs_pipeline_op_t *cur_op;  /* Current pipeline op */
    int32_t cur_i;              /* Index in current result */
    int32_t ran_since_merge;    /* Index in current op */
    bool no_readonly;           /* Is pipeline in readonly mode */
} ecs_pipeline_state_t;

typedef struct EcsPipeline {
    /* Stable ptr so threads can safely access while entity/components move */
    ecs_pipeline_state_t *state;
} EcsPipeline;

////////////////////////////////////////////////////////////////////////////////
//// Pipeline API
////////////////////////////////////////////////////////////////////////////////

bool flecs_pipeline_update(
    ecs_world_t *world,
    ecs_pipeline_state_t *pq,
    bool start_of_frame);

void flecs_run_pipeline(
    ecs_world_t *world,
    ecs_pipeline_state_t *pq,
    ecs_ftime_t delta_time);

////////////////////////////////////////////////////////////////////////////////
//// Worker API
////////////////////////////////////////////////////////////////////////////////

bool flecs_worker_begin(
    ecs_world_t *world,
    ecs_stage_t *stage,
    ecs_pipeline_state_t *pq,
    bool start_of_frame);

void flecs_worker_end(
    ecs_world_t *world,
    ecs_stage_t *stage);

bool flecs_worker_sync(
    ecs_world_t *world,
    ecs_stage_t *stage,
    ecs_pipeline_state_t *pq,
    ecs_pipeline_op_t **cur_op,
    int32_t *cur_i);

void flecs_workers_progress(
    ecs_world_t *world,
    ecs_pipeline_state_t *pq,
    ecs_ftime_t delta_time);

void flecs_create_worker_threads(
    ecs_world_t *world);

bool flecs_join_worker_threads(
    ecs_world_t *world);

#endif


typedef struct ecs_worker_state_t {
    ecs_stage_t *stage;
    ecs_pipeline_state_t *pq;
} ecs_worker_state_t;

/* Worker thread */
static
void* flecs_worker(void *arg) {
    ecs_worker_state_t *state = arg;
    ecs_stage_t *stage = state->stage;
    ecs_pipeline_state_t *pq = state->pq;
    ecs_world_t *world = stage->world;

    ecs_poly_assert(world, ecs_world_t);
    ecs_poly_assert(stage, ecs_stage_t);

    ecs_dbg_2("worker %d: start", stage->id);

    /* Start worker, increase counter so main thread knows how many
     * workers are ready */
    ecs_os_mutex_lock(world->sync_mutex);
    world->workers_running ++;

    if (!(world->flags & EcsWorldQuitWorkers)) {
        ecs_os_cond_wait(world->worker_cond, world->sync_mutex);
    }

    ecs_os_mutex_unlock(world->sync_mutex);

    while (!(world->flags & EcsWorldQuitWorkers)) {
        ecs_entity_t old_scope = ecs_set_scope((ecs_world_t*)stage, 0);

        ecs_dbg_3("worker %d: run", stage->id);
 
        flecs_run_pipeline((ecs_world_t*)stage, pq, world->info.delta_time);

        ecs_set_scope((ecs_world_t*)stage, old_scope);
    }

    ecs_dbg_2("worker %d: finalizing", stage->id);

    ecs_os_mutex_lock(world->sync_mutex);
    world->workers_running --;
    ecs_os_mutex_unlock(world->sync_mutex);

    ecs_dbg_2("worker %d: stop", stage->id);

    ecs_os_free(state);

    return NULL;
}

static
bool flecs_is_multithreaded(
    ecs_world_t *world)
{
    ecs_poly_assert(world, ecs_world_t);
    return ecs_get_stage_count(world) > 1;
}

static
bool flecs_is_main_thread(
    ecs_stage_t *stage)
{
    return !stage->id;
}

/* Start threads */
void flecs_create_worker_threads(
    ecs_world_t *world)
{
    ecs_poly_assert(world, ecs_world_t);
    int32_t stages = ecs_get_stage_count(world);

    for (int32_t i = 1; i < stages; i ++) {
        ecs_stage_t *stage = (ecs_stage_t*)ecs_get_stage(world, i);
        ecs_assert(stage != NULL, ECS_INTERNAL_ERROR, NULL);
        ecs_poly_assert(stage, ecs_stage_t);

        ecs_entity_t pipeline = world->pipeline;
        ecs_assert(pipeline != 0, ECS_INVALID_OPERATION, NULL);
        const EcsPipeline *pqc = ecs_get(world, pipeline, EcsPipeline);
        ecs_assert(pqc != NULL, ECS_INVALID_OPERATION, NULL);
        ecs_pipeline_state_t *pq = pqc->state;
        ecs_assert(pq != NULL, ECS_INTERNAL_ERROR, NULL);

        ecs_worker_state_t *state = ecs_os_calloc_t(ecs_worker_state_t);
        state->stage = stage;
        state->pq = pq;

        ecs_assert(stage->thread == 0, ECS_INTERNAL_ERROR, NULL);
        if (ecs_using_task_threads(world))
        {
            /* workers are using tasks in an external task manager provided to the OS API */
            stage->thread = ecs_os_task_new(flecs_worker, state);
        }
        else
        {
            /* workers are using long-running os threads */
            stage->thread = ecs_os_thread_new(flecs_worker, state);
        }
        ecs_assert(stage->thread != 0, ECS_OPERATION_FAILED, NULL);
    }
}

static
void flecs_start_workers(
    ecs_world_t *world,
    int32_t threads)
{
    ecs_set_stage_count(world, threads);

    ecs_assert(ecs_get_stage_count(world) == threads, ECS_INTERNAL_ERROR, NULL);

    if (!ecs_using_task_threads(world))
    {
        flecs_create_worker_threads(world);
    }
}

/* Wait until all workers are running */
static
void flecs_wait_for_workers(
    ecs_world_t *world)
{
    ecs_poly_assert(world, ecs_world_t);

    int32_t stage_count = ecs_get_stage_count(world);
    if (stage_count <= 1) {
        return;
    }

    bool wait = true;
    do {
        ecs_os_mutex_lock(world->sync_mutex);
        if (world->workers_running == (stage_count - 1)) {
            wait = false;
        }
        ecs_os_mutex_unlock(world->sync_mutex);
    } while (wait);
}

/* Wait until all threads are waiting on sync point */
static
void flecs_wait_for_sync(
    ecs_world_t *world)
{
    int32_t stage_count = ecs_get_stage_count(world);
    if (stage_count <= 1) {
        return;
    }

    ecs_dbg_3("#[bold]pipeline: waiting for worker sync");

    ecs_os_mutex_lock(world->sync_mutex);
    if (world->workers_waiting != (stage_count - 1)) {
        ecs_os_cond_wait(world->sync_cond, world->sync_mutex);
    }

    /* We shouldn't have been signalled unless all workers are waiting on sync */
    ecs_assert(world->workers_waiting == (stage_count - 1), 
        ECS_INTERNAL_ERROR, NULL);

    world->workers_waiting = 0;
    ecs_os_mutex_unlock(world->sync_mutex);

    ecs_dbg_3("#[bold]pipeline: workers synced");
}

/* Synchronize workers */
static
void flecs_sync_worker(
    ecs_world_t *world)
{
    int32_t stage_count = ecs_get_stage_count(world);
    if (stage_count <= 1) {
        return;
    }

    /* Signal that thread is waiting */
    ecs_os_mutex_lock(world->sync_mutex);
    if (++ world->workers_waiting == (stage_count - 1)) {
        /* Only signal main thread when all threads are waiting */
        ecs_os_cond_signal(world->sync_cond);
    }

    /* Wait until main thread signals that thread can continue */
    ecs_os_cond_wait(world->worker_cond, world->sync_mutex);
    ecs_os_mutex_unlock(world->sync_mutex);
}

/* Signal workers that they can start/resume work */
static
void flecs_signal_workers(
    ecs_world_t *world)
{
    int32_t stage_count = ecs_get_stage_count(world);
    if (stage_count <= 1) {
        return;
    }

    ecs_dbg_3("#[bold]pipeline: signal workers");
    ecs_os_mutex_lock(world->sync_mutex);
    ecs_os_cond_broadcast(world->worker_cond);
    ecs_os_mutex_unlock(world->sync_mutex);
}
 
bool flecs_join_worker_threads(
    ecs_world_t *world)
{
    ecs_poly_assert(world, ecs_world_t);
    bool threads_active = false;

    /* Test if threads are created. Cannot use workers_running, since this is
     * a potential race if threads haven't spun up yet. */
    ecs_stage_t *stages = world->stages;
    int i, count = world->stage_count;
    for (i = 1; i < count; i ++) {
        ecs_stage_t *stage = &stages[i];
        if (stage->thread) {
            threads_active = true;
            break;
        }
        stage->thread = 0;
    };

    /* If no threads are active, just return */
    if (!threads_active) {
        return false;
    }

    /* Make sure all threads are running, to ensure they catch the signal */
    flecs_wait_for_workers(world);

    /* Signal threads should quit */
    world->flags |= EcsWorldQuitWorkers;
    flecs_signal_workers(world);

    /* Join all threads with main */
    for (i = 1; i < count; i ++) {
        if (ecs_using_task_threads(world))
        {
            /* Join using the override provided */
            ecs_os_task_join(stages[i].thread);
        }
        else
        {
            ecs_os_thread_join(stages[i].thread);
        }
        stages[i].thread = 0;
    }

    world->flags &= ~EcsWorldQuitWorkers;
    ecs_assert(world->workers_running == 0, ECS_INTERNAL_ERROR, NULL);

    return true;
}

/** Stop workers */
static
bool ecs_stop_threads(
    ecs_world_t *world)
{
    /* Join all existing worker threads */
    if (flecs_join_worker_threads(world))
    {
        /* Deinitialize stages */
        ecs_set_stage_count(world, 1);

        return true;
    }

    return false;
}

/* -- Private functions -- */
bool flecs_worker_begin(
    ecs_world_t *world,
    ecs_stage_t *stage,
    ecs_pipeline_state_t *pq,
    bool start_of_frame)
{
    ecs_poly_assert(world, ecs_world_t);
    ecs_poly_assert(stage, ecs_stage_t);
    bool main_thread = flecs_is_main_thread(stage);
    bool multi_threaded = flecs_is_multithreaded(world);

    if (main_thread) {
        if (ecs_stage_is_readonly(world)) {
            ecs_assert(!pq->no_readonly, ECS_INTERNAL_ERROR, NULL);
            ecs_readonly_end(world);
            pq->no_readonly = false;
        }

        flecs_pipeline_update(world, pq, start_of_frame);
    }

    ecs_pipeline_op_t *cur_op = pq->cur_op;
    if (main_thread && (cur_op != NULL)) {
        pq->no_readonly = cur_op->no_readonly;
        if (!cur_op->no_readonly) {
            ecs_readonly_begin(world);
        }

        ECS_BIT_COND(world->flags, EcsWorldMultiThreaded, 
            cur_op->multi_threaded);
        ecs_assert(world->workers_waiting == 0, 
            ECS_INTERNAL_ERROR, NULL);
    }

    if (main_thread && multi_threaded) {
        flecs_signal_workers(world);
    }

    return pq->cur_op != NULL;
}

void flecs_worker_end(
    ecs_world_t *world,
    ecs_stage_t *stage)
{
    ecs_poly_assert(world, ecs_world_t);
    ecs_poly_assert(stage, ecs_stage_t);

    if (flecs_is_multithreaded(world)) {
        if (flecs_is_main_thread(stage)) {
            flecs_wait_for_sync(world);
        } else {
            flecs_sync_worker(world);
        }
    }

    if (flecs_is_main_thread(stage)) {
        if (ecs_stage_is_readonly(world)) {
            ecs_readonly_end(world);
        }
    }
}

bool flecs_worker_sync(
    ecs_world_t *world,
    ecs_stage_t *stage,
    ecs_pipeline_state_t *pq,
    ecs_pipeline_op_t **cur_op,
    int32_t *cur_i)
{
    ecs_assert(pq != NULL, ECS_INTERNAL_ERROR, NULL);
    ecs_assert(pq->cur_op != NULL, ECS_INTERNAL_ERROR, NULL);
    bool main_thread = flecs_is_main_thread(stage);

    /* Synchronize workers */
    flecs_worker_end(world, stage);

    /* Store the current state of the schedule after we synchronized the
     * threads, to avoid race conditions. */
    if (main_thread) {
        pq->cur_op = *cur_op;
        pq->cur_i = *cur_i;
    }

    /* Prepare state for running the next part of the schedule */
    bool result = flecs_worker_begin(world, stage, pq, false);
    *cur_op = pq->cur_op;
    *cur_i = pq->cur_i;
    return result;
}

void flecs_workers_progress(
    ecs_world_t *world,
    ecs_pipeline_state_t *pq,
    ecs_ftime_t delta_time)
{
    ecs_poly_assert(world, ecs_world_t);
    ecs_assert(!ecs_is_deferred(world), ECS_INVALID_OPERATION, NULL);

    /* Make sure workers are running and ready */
    flecs_wait_for_workers(world);

    /* Run pipeline on main thread */
    ecs_world_t *stage = ecs_get_stage(world, 0);
    ecs_entity_t old_scope = ecs_set_scope((ecs_world_t*)stage, 0);
    flecs_run_pipeline(stage, pq, delta_time);
    ecs_set_scope((ecs_world_t*)stage, old_scope);
}

static
void flecs_set_threads_internal(
    ecs_world_t *world,
    int32_t threads,
    bool use_task_api)
{
    ecs_assert(threads <= 1 || (use_task_api ? ecs_os_has_task_support() : ecs_os_has_threading()), ECS_MISSING_OS_API, NULL);
    
    int32_t stage_count = ecs_get_stage_count(world);
    bool worker_method_changed = (use_task_api != world->workers_use_task_api);
    
    if (stage_count != threads || worker_method_changed) {
        /* Stop existing threads */
        if (stage_count > 1) {
            if (ecs_stop_threads(world)) {
                ecs_os_cond_free(world->worker_cond);
                ecs_os_cond_free(world->sync_cond);
                ecs_os_mutex_free(world->sync_mutex);
            }
        }

        /* Adopt the desired API for worker creation & join */
        world->workers_use_task_api = use_task_api;

        /* Start threads if number of threads > 1 */
        if (threads > 1) {
            world->worker_cond = ecs_os_cond_new();
            world->sync_cond = ecs_os_cond_new();
            world->sync_mutex = ecs_os_mutex_new();
            flecs_start_workers(world, threads);
        }
    }
}

/* -- Public functions -- */

void ecs_set_threads(
    ecs_world_t *world,
    int32_t threads)
{
    flecs_set_threads_internal(world, threads, false /* use thread API*/);
}

void ecs_set_task_threads(
    ecs_world_t *world,
    int32_t task_threads)
{
    flecs_set_threads_internal(world, task_threads, true /* use task API*/);
}

bool ecs_using_task_threads(
    ecs_world_t *world)
{
    return world->workers_use_task_api;
}

#endif

/**
 * @file addons/ipeline/pipeline.c
 * @brief Functions for building and running pipelines.
 */


#ifdef FLECS_PIPELINE

static void flecs_pipeline_free(
    ecs_pipeline_state_t *p) 
{
    if (p) {
        ecs_world_t *world = p->query->filter.world;
        ecs_allocator_t *a = &world->allocator;
        ecs_vec_fini_t(a, &p->ops, ecs_pipeline_op_t);
        ecs_vec_fini_t(a, &p->systems, ecs_entity_t);
        ecs_os_free(p->iters);
        ecs_query_fini(p->query);
        ecs_os_free(p);
    }
}

static ECS_MOVE(EcsPipeline, dst, src, {
    flecs_pipeline_free(dst->state);
    dst->state = src->state;
    src->state = NULL;
})

static ECS_DTOR(EcsPipeline, ptr, {
    flecs_pipeline_free(ptr->state);
})

typedef enum ecs_write_kind_t {
    WriteStateNone = 0,
    WriteStateToStage,
} ecs_write_kind_t;

typedef struct ecs_write_state_t {
    bool write_barrier;
    ecs_map_t ids;
    ecs_map_t wildcard_ids;
} ecs_write_state_t;

static
ecs_write_kind_t flecs_pipeline_get_write_state(
    ecs_write_state_t *write_state,
    ecs_id_t id)
{
    ecs_write_kind_t result = WriteStateNone;

    if (write_state->write_barrier) {
        /* Any component could have been written */
        return WriteStateToStage;
    }

    if (id == EcsWildcard) {
        /* Using a wildcard for id indicates read barrier. Return true if any
         * components could have been staged */
        if (ecs_map_count(&write_state->ids) || 
            ecs_map_count(&write_state->wildcard_ids)) 
        {
            return WriteStateToStage;
        }
    }

    if (!ecs_id_is_wildcard(id)) {
        if (ecs_map_get(&write_state->ids, id)) {
            result = WriteStateToStage;
        }
    } else {
        ecs_map_iter_t it = ecs_map_iter(&write_state->ids);
        while (ecs_map_next(&it)) {
            if (ecs_id_match(ecs_map_key(&it), id)) {
                return WriteStateToStage;
            }
        }
    }

    if (ecs_map_count(&write_state->wildcard_ids)) {
        ecs_map_iter_t it = ecs_map_iter(&write_state->wildcard_ids);
        while (ecs_map_next(&it)) {
            if (ecs_id_match(id, ecs_map_key(&it))) {
                return WriteStateToStage;
            }
        }
    }

    return result;
}

static
void flecs_pipeline_set_write_state(
    ecs_write_state_t *write_state,
    ecs_id_t id)
{
    if (id == EcsWildcard) {
        /* If writing to wildcard, flag all components as written */
        write_state->write_barrier = true;
        return;
    }

    ecs_map_t *ids;
    if (ecs_id_is_wildcard(id)) {
        ids = &write_state->wildcard_ids;
    } else {
        ids = &write_state->ids;
    }

    ecs_map_ensure(ids, id)[0] = true;
}

static
void flecs_pipeline_reset_write_state(
    ecs_write_state_t *write_state)
{
    ecs_map_clear(&write_state->ids);
    ecs_map_clear(&write_state->wildcard_ids);
    write_state->write_barrier = false;
}

static
bool flecs_pipeline_check_term(
    ecs_world_t *world,
    ecs_term_t *term,
    bool is_active,
    ecs_write_state_t *write_state)    
{
    (void)world;

    ecs_term_id_t *src = &term->src;
    if (src->flags & EcsInOutNone) {
        return false;
    }

    ecs_id_t id = term->id;
    ecs_oper_kind_t oper = term->oper;
    ecs_inout_kind_t inout = term->inout;
    bool from_any = ecs_term_match_0(term);
    bool from_this = ecs_term_match_this(term);
    bool is_shared = !from_any && (!from_this || !(src->flags & EcsSelf));

    ecs_write_kind_t ws = flecs_pipeline_get_write_state(write_state, id);

    if (from_this && ws >= WriteStateToStage) {
        /* A staged write could have happened for an id that's matched on the
         * main storage. Even if the id isn't read, still insert a merge so that
         * a write to the main storage after the staged write doesn't get 
         * overwritten. */
        return true;
    }

    if (inout == EcsInOutDefault) {
        if (from_any) {
            /* If no inout kind is specified for terms without a source, this is
             * not interpreted as a read/write annotation but just a (component)
             * id that's passed to a system. */
            return false;
        } else if (is_shared) {
            inout = EcsIn;
        } else {
            /* Default for owned terms is InOut */
            inout = EcsInOut;
        }
    }

    if (oper == EcsNot && inout == EcsOut) {
        /* If a Not term is combined with Out, it signals that the system 
         * intends to add a component that the entity doesn't yet have */
        from_any = true;
    }

    if (from_any) {
        switch(inout) {
        case EcsOut:
        case EcsInOut:
            if (is_active) {
                /* Only flag component as written if system is active */
                flecs_pipeline_set_write_state(write_state, id);
            }
            break;
        default:
            break;
        }

        switch(inout) {
        case EcsIn:
        case EcsInOut:
            if (ws == WriteStateToStage) {
                /* If a system does a get/get_mut, the component is fetched from
                 * the main store so it must be merged first */
                return true;
            }
        default:
            break;
        }
    }

    return false;
}

static
bool flecs_pipeline_check_terms(
    ecs_world_t *world,
    ecs_filter_t *filter,
    bool is_active,
    ecs_write_state_t *ws)
{
    bool needs_merge = false;
    ecs_term_t *terms = filter->terms;
    int32_t t, term_count = filter->term_count;

    /* Check This terms first. This way if a term indicating writing to a stage
     * was added before the term, it won't cause merging. */
    for (t = 0; t < term_count; t ++) {
        ecs_term_t *term = &terms[t];
        if (ecs_term_match_this(term)) {
            needs_merge |= flecs_pipeline_check_term(world, term, is_active, ws);
        }
    }

    /* Now check staged terms */
    for (t = 0; t < term_count; t ++) {
        ecs_term_t *term = &terms[t];
        if (!ecs_term_match_this(term)) {
            needs_merge |= flecs_pipeline_check_term(world, term, is_active, ws);
        }
    }

    return needs_merge;
}

static
EcsPoly* flecs_pipeline_term_system(
    ecs_iter_t *it)
{
    int32_t index = ecs_search(it->real_world, it->table, 
        ecs_poly_id(EcsSystem), 0);
    ecs_assert(index != -1, ECS_INTERNAL_ERROR, NULL);
    EcsPoly *poly = ecs_table_get_column(it->table, index, it->offset);
    ecs_assert(poly != NULL, ECS_INTERNAL_ERROR, NULL);
    return poly;
}

static
bool flecs_pipeline_build(
    ecs_world_t *world,
    ecs_pipeline_state_t *pq)
{
    ecs_iter_t it = ecs_query_iter(world, pq->query);

    if (pq->match_count == pq->query->match_count) {
        /* No need to rebuild the pipeline */
        ecs_iter_fini(&it);
        return false;
    }

    world->info.pipeline_build_count_total ++;
    pq->rebuild_count ++;

    ecs_allocator_t *a = &world->allocator;
    ecs_pipeline_op_t *op = NULL;
    ecs_write_state_t ws = {0};
    ecs_map_init(&ws.ids, a);
    ecs_map_init(&ws.wildcard_ids, a);

    ecs_vec_reset_t(a, &pq->ops, ecs_pipeline_op_t);
    ecs_vec_reset_t(a, &pq->systems, ecs_entity_t);

    bool multi_threaded = false;
    bool no_readonly = false;
    bool first = true;

    /* Iterate systems in pipeline, add ops for running / merging */
    while (ecs_query_next(&it)) {
        EcsPoly *poly = flecs_pipeline_term_system(&it);
        bool is_active = ecs_table_get_index(world, it.table, EcsEmpty) == -1;

        int32_t i;
        for (i = 0; i < it.count; i ++) {
            ecs_system_t *sys = ecs_poly(poly[i].poly, ecs_system_t);
            ecs_query_t *q = sys->query;

            bool needs_merge = false;
            needs_merge = flecs_pipeline_check_terms(
                world, &q->filter, is_active, &ws);

            if (is_active) {
                if (first) {
                    multi_threaded = sys->multi_threaded;
                    no_readonly = sys->no_readonly;
                    first = false;
                }

                if (sys->multi_threaded != multi_threaded) {
                    needs_merge = true;
                    multi_threaded = sys->multi_threaded;
                }
                if (sys->no_readonly != no_readonly) {
                    needs_merge = true;
                    no_readonly = sys->no_readonly;
                }
            }

            if (no_readonly) {
                needs_merge = true;
            }

            if (needs_merge) {
                /* After merge all components will be merged, so reset state */
                flecs_pipeline_reset_write_state(&ws);

                /* An inactive system can insert a merge if one of its 
                 * components got written, which could make the system 
                 * active. If this is the only system in the pipeline operation,
                 * it results in an empty operation when we get here. If that's
                 * the case, reuse the empty operation for the next op. */
                if (op && op->count) {
                    op = NULL;
                }

                /* Re-evaluate columns to set write flags if system is active.
                 * If system is inactive, it can't write anything and so it
                 * should not insert unnecessary merges.  */
                needs_merge = false;
                if (is_active) {
                    needs_merge = flecs_pipeline_check_terms(
                        world, &q->filter, true, &ws);
                }

                /* The component states were just reset, so if we conclude that
                 * another merge is needed something is wrong. */
                ecs_assert(needs_merge == false, ECS_INTERNAL_ERROR, NULL);        
            }

            if (!op) {
                op = ecs_vec_append_t(a, &pq->ops, ecs_pipeline_op_t);
                op->offset = ecs_vec_count(&pq->systems);
                op->count = 0;
                op->multi_threaded = false;
                op->no_readonly = false;
            }

            /* Don't increase count for inactive systems, as they are ignored by
             * the query used to run the pipeline. */
            if (is_active) {
                ecs_vec_append_t(a, &pq->systems, ecs_entity_t)[0] = 
                    it.entities[i];
                if (!op->count) {
                    op->multi_threaded = multi_threaded;
                    op->no_readonly = no_readonly;
                }
                op->count ++;
            }
        }
    }

    if (op && !op->count && ecs_vec_count(&pq->ops) > 1) {
        ecs_vec_remove_last(&pq->ops);
    }

    ecs_map_fini(&ws.ids);
    ecs_map_fini(&ws.wildcard_ids);

    op = ecs_vec_first_t(&pq->ops, ecs_pipeline_op_t);

    if (!op) {
        ecs_dbg("#[green]pipeline#[reset] is empty");
        return true;
    } else {
        /* Add schedule to debug tracing */
        ecs_dbg("#[bold]pipeline rebuild");
        ecs_log_push_1();

        ecs_dbg("#[green]schedule#[reset]: threading: %d, staging: %d:", 
            op->multi_threaded, !op->no_readonly);
        ecs_log_push_1();

        int32_t i, count = ecs_vec_count(&pq->systems);
        int32_t op_index = 0, ran_since_merge = 0;
        ecs_entity_t *systems = ecs_vec_first_t(&pq->systems, ecs_entity_t);
        for (i = 0; i < count; i ++) {
            ecs_entity_t system = systems[i];
            const EcsPoly *poly = ecs_get_pair(world, system, EcsPoly, EcsSystem);
            ecs_assert(poly != NULL, ECS_INTERNAL_ERROR, NULL);
            ecs_system_t *sys = ecs_poly(poly->poly, ecs_system_t);

#ifdef FLECS_LOG_1
            char *path = ecs_get_fullpath(world, system);
            const char *doc_name = NULL;
#ifdef FLECS_DOC
            const EcsDocDescription *doc_name_id = ecs_get_pair(world, system, 
                EcsDocDescription, EcsName);
            if (doc_name_id) {
                doc_name = doc_name_id->value;
            }
#endif
            if (doc_name) {
                ecs_dbg("#[green]system#[reset] %s (%s)", path, doc_name);
            } else {
                ecs_dbg("#[green]system#[reset] %s", path);
            }
            ecs_os_free(path);
#endif

            ecs_assert(op[op_index].offset + ran_since_merge == i,
                ECS_INTERNAL_ERROR, NULL);

            ran_since_merge ++;
            if (ran_since_merge == op[op_index].count) {
                ecs_dbg("#[magenta]merge#[reset]");
                ecs_log_pop_1();
                ran_since_merge = 0;
                op_index ++;
                if (op_index < ecs_vec_count(&pq->ops)) {
                    ecs_dbg(
                        "#[green]schedule#[reset]: "
                        "threading: %d, staging: %d:",
                        op[op_index].multi_threaded, 
                        !op[op_index].no_readonly);
                }
                ecs_log_push_1();
            }

            if (sys->last_frame == (world->info.frame_count_total + 1)) {
                if (op_index < ecs_vec_count(&pq->ops)) {
                    pq->cur_op = &op[op_index];
                    pq->cur_i = i;
                } else {
                    pq->cur_op = NULL;
                    pq->cur_i = 0;
                }
            }
        }

        ecs_log_pop_1();
        ecs_log_pop_1();
    }

    pq->match_count = pq->query->match_count;

    ecs_assert(pq->cur_op <= ecs_vec_last_t(&pq->ops, ecs_pipeline_op_t),
        ECS_INTERNAL_ERROR, NULL);

    return true;
}

static
void flecs_pipeline_next_system(
    ecs_pipeline_state_t *pq)
{
    if (!pq->cur_op) {
        return;
    }

    pq->cur_i ++;
    if (pq->cur_i >= (pq->cur_op->offset + pq->cur_op->count)) {
        pq->cur_op ++;
        if (pq->cur_op > ecs_vec_last_t(&pq->ops, ecs_pipeline_op_t)) {
            pq->cur_op = NULL;
        }
    }    
}

bool flecs_pipeline_update(
    ecs_world_t *world,
    ecs_pipeline_state_t *pq,
    bool start_of_frame)
{
    ecs_poly_assert(world, ecs_world_t);
    ecs_assert(!(world->flags & EcsWorldReadonly), ECS_INVALID_OPERATION, NULL);

    /* If any entity mutations happened that could have affected query matching
     * notify appropriate queries so caches are up to date. This includes the
     * pipeline query. */
    if (start_of_frame) {
        ecs_run_aperiodic(world, 0);
    }

    ecs_assert(pq != NULL, ECS_INTERNAL_ERROR, NULL);
    ecs_assert(pq->query != NULL, ECS_INTERNAL_ERROR, NULL);

    bool rebuilt = flecs_pipeline_build(world, pq);
    if (start_of_frame) {
        /* Initialize iterators */
        int32_t i, count = pq->iter_count;
        for (i = 0; i < count; i ++) {
            ecs_world_t *stage = ecs_get_stage(world, i);
            pq->iters[i] = ecs_query_iter(stage, pq->query);
        }
        pq->cur_op = ecs_vec_first_t(&pq->ops, ecs_pipeline_op_t);
        pq->cur_i = 0;
    } else {
        flecs_pipeline_next_system(pq);
    }

    return rebuilt;
}

void ecs_run_pipeline(
    ecs_world_t *world,
    ecs_entity_t pipeline,
    ecs_ftime_t delta_time)
{
    if (!pipeline) {
        pipeline = world->pipeline;
    }

    EcsPipeline *pq = (EcsPipeline*)ecs_get(world, pipeline, EcsPipeline);
    flecs_pipeline_update(world, pq->state, true);
    flecs_run_pipeline((ecs_world_t*)flecs_stage_from_world(&world), 
        pq->state, delta_time);
}

void flecs_run_pipeline(
    ecs_world_t *world,
    ecs_pipeline_state_t *pq,
    ecs_ftime_t delta_time)
{
    ecs_assert(world != NULL, ECS_INVALID_OPERATION, NULL);
    ecs_assert(pq != NULL, ECS_INTERNAL_ERROR, NULL);
    ecs_assert(pq->query != NULL, ECS_INTERNAL_ERROR, NULL);
    ecs_poly_assert(world, ecs_stage_t);

    ecs_stage_t *stage = flecs_stage_from_world(&world);  
    int32_t stage_index = ecs_get_stage_id(stage->thread_ctx);
    int32_t stage_count = ecs_get_stage_count(world);

    if (!flecs_worker_begin(world, stage, pq, true)) {
        return;
    }

    ecs_time_t st = {0};
    bool main_thread = !stage_index;
    bool measure_time = main_thread && (world->flags & EcsWorldMeasureSystemTime);
    ecs_pipeline_op_t *op = ecs_vec_first_t(&pq->ops, ecs_pipeline_op_t);
    int32_t i = 0;

    do {
        int32_t count = ecs_vec_count(&pq->systems);
        ecs_entity_t *systems = ecs_vec_first_t(&pq->systems, ecs_entity_t);
        int32_t ran_since_merge = i - op->offset;

        if (i == count) {
            break;
        }

        if (measure_time) {
            ecs_time_measure(&st);
        }

        for (; i < count; i ++) {
            /* Run system if:
             * - this is the main thread, or if
             * - the system is multithreaded 
             */
            if (main_thread || op->multi_threaded) {
                ecs_entity_t system = systems[i];
                const EcsPoly *poly = ecs_get_pair(world, system, EcsPoly, EcsSystem);
                ecs_assert(poly != NULL, ECS_INTERNAL_ERROR, NULL);
                ecs_system_t *sys = ecs_poly(poly->poly, ecs_system_t);

                /* Keep track of the last frame for which the system has ran, so we
                * know from where to resume the schedule in case the schedule 
                * changes during a merge. */
                sys->last_frame = world->info.frame_count_total + 1;

                ecs_stage_t *s = NULL;
                if (!op->no_readonly) {
                    /* If system is no_readonly it operates on the actual world, not
                     * the stage. Only pass stage to system if it's readonly. */
                    s = stage;
                }

                ecs_run_intern(world, s, system, sys, stage_index, 
                    stage_count, delta_time, 0, 0, NULL);
            }

            world->info.systems_ran_frame ++;
            ran_since_merge ++;

            if (ran_since_merge == op->count) {
                /* Merge */
                break;
            }
        }

        if (measure_time) {
            /* Don't include merge time in system time */
            world->info.system_time_total += 
                (ecs_ftime_t)ecs_time_measure(&st);
        }

        /* Synchronize workers, rebuild pipeline if necessary. Pass current op
         * and system index to function, so we know where to resume from. */
    } while (flecs_worker_sync(world, stage, pq, &op, &i));

    if (measure_time) {
        world->info.system_time_total += (ecs_ftime_t)ecs_time_measure(&st);
    }

    flecs_worker_end(world, stage);

    return;
}

static
void flecs_run_startup_systems(
    ecs_world_t *world)
{
    ecs_id_record_t *idr = flecs_id_record_get(world, 
        ecs_dependson(EcsOnStart));
    if (!idr || !flecs_table_cache_count(&idr->cache)) {
        /* Don't bother creating startup pipeline if no systems exist */
        return;
    }

    ecs_dbg_2("#[bold]startup#[reset]");
    ecs_log_push_2();
    int32_t stage_count = world->stage_count;
    world->stage_count = 1; /* Prevents running startup systems on workers */

    /* Creating a pipeline is relatively expensive, but this only happens 
     * for the first frame. The startup pipeline is deleted afterwards, which
     * eliminates the overhead of keeping its query cache in sync. */
    ecs_dbg_2("#[bold]create startup pipeline#[reset]");
    ecs_log_push_2();
    ecs_entity_t start_pip = ecs_pipeline_init(world, &(ecs_pipeline_desc_t){
        .query = {
            .filter.terms = {
                { .id = EcsSystem },
                { .id = EcsPhase, .src.flags = EcsCascade, .src.trav = EcsDependsOn },
                { .id = ecs_dependson(EcsOnStart), .src.trav = EcsDependsOn },
                { .id = EcsDisabled, .src.flags = EcsUp, .src.trav = EcsDependsOn, .oper = EcsNot },
                { .id = EcsDisabled, .src.flags = EcsUp, .src.trav = EcsChildOf, .oper = EcsNot }
            },
            .order_by = flecs_entity_compare
        }
    });
    ecs_log_pop_2();

    /* Run & delete pipeline */
    ecs_dbg_2("#[bold]run startup systems#[reset]");
    ecs_log_push_2();
    ecs_assert(start_pip != 0, ECS_INTERNAL_ERROR, NULL);
    const EcsPipeline *p = ecs_get(world, start_pip, EcsPipeline);
    ecs_check(p != NULL, ECS_INVALID_OPERATION, NULL);
    flecs_workers_progress(world, p->state, 0);
    ecs_log_pop_2();

    ecs_dbg_2("#[bold]delete startup pipeline#[reset]");
    ecs_log_push_2();
    ecs_delete(world, start_pip);
    ecs_log_pop_2();

    world->stage_count = stage_count;
    ecs_log_pop_2();

error:
    return;
}

bool ecs_progress(
    ecs_world_t *world,
    ecs_ftime_t user_delta_time)
{
    ecs_ftime_t delta_time = ecs_frame_begin(world, user_delta_time);
    
    /* If this is the first frame, run startup systems */
    if (world->info.frame_count_total == 0) {
        flecs_run_startup_systems(world);
    }

    /* create any worker task threads request */
    if (ecs_using_task_threads(world))
    {
        flecs_create_worker_threads(world);
    }

    ecs_dbg_3("#[bold]progress#[reset](dt = %.2f)", (double)delta_time);
    ecs_log_push_3();
    const EcsPipeline *p = ecs_get(world, world->pipeline, EcsPipeline);
    ecs_check(p != NULL, ECS_INVALID_OPERATION, NULL);
    flecs_workers_progress(world, p->state, delta_time);
    ecs_log_pop_3();

    ecs_frame_end(world);

    if (ecs_using_task_threads(world))
    {
        /* task threads were temporary and may now be joined */
        flecs_join_worker_threads(world);
    }

    return !ECS_BIT_IS_SET(world->flags, EcsWorldQuit);
error:
    return false;
}

void ecs_set_time_scale(
    ecs_world_t *world,
    ecs_ftime_t scale)
{
    world->info.time_scale = scale;
}

void ecs_reset_clock(
    ecs_world_t *world)
{
    world->info.world_time_total = 0;
    world->info.world_time_total_raw = 0;
}

void ecs_set_pipeline(
    ecs_world_t *world,
    ecs_entity_t pipeline)
{
    ecs_poly_assert(world, ecs_world_t);
    ecs_check( ecs_get(world, pipeline, EcsPipeline) != NULL, 
        ECS_INVALID_PARAMETER, "not a pipeline");

    int32_t thread_count = ecs_get_stage_count(world);
    if (thread_count > 1) {
        ecs_set_threads(world, 1);
    }
    world->pipeline = pipeline;
    if (thread_count > 1) {
        ecs_set_threads(world, thread_count);
    }
error:
    return;
}

ecs_entity_t ecs_get_pipeline(
    const ecs_world_t *world)
{
    ecs_check(world != NULL, ECS_INVALID_PARAMETER, NULL);
    world = ecs_get_world(world);
    return world->pipeline;
error:
    return 0;
}

ecs_entity_t ecs_pipeline_init(
    ecs_world_t *world,
    const ecs_pipeline_desc_t *desc)
{
    ecs_poly_assert(world, ecs_world_t);
    ecs_assert(desc != NULL, ECS_INVALID_PARAMETER, NULL);

    ecs_entity_t result = desc->entity;
    if (!result) {
        result = ecs_new(world, 0);
    }

    ecs_query_desc_t qd = desc->query;
    if (!qd.order_by) {
        qd.order_by = flecs_entity_compare;
    }
    qd.filter.entity = result;

    ecs_query_t *query = ecs_query_init(world, &qd);
    if (!query) {
        ecs_delete(world, result);
        return 0;
    }

    ecs_assert(query->filter.terms[0].id == EcsSystem,
        ECS_INVALID_PARAMETER, NULL);

    ecs_pipeline_state_t *pq = ecs_os_calloc_t(ecs_pipeline_state_t);
    pq->query = query;
    pq->match_count = -1;
    pq->idr_inactive = flecs_id_record_ensure(world, EcsEmpty);
    ecs_set(world, result, EcsPipeline, { pq });

    return result;
}

/* -- Module implementation -- */

static
void FlecsPipelineFini(
    ecs_world_t *world,
    void *ctx)
{
    (void)ctx;
    if (ecs_get_stage_count(world)) {
        ecs_set_threads(world, 0);
    }

    ecs_assert(world->workers_running == 0, ECS_INTERNAL_ERROR, NULL);
}

#define flecs_bootstrap_phase(world, phase, depends_on)\
    flecs_bootstrap_tag(world, phase);\
    _flecs_bootstrap_phase(world, phase, depends_on)
static
void _flecs_bootstrap_phase(
    ecs_world_t *world,
    ecs_entity_t phase,
    ecs_entity_t depends_on)
{
    ecs_add_id(world, phase, EcsPhase);
    if (depends_on) {
        ecs_add_pair(world, phase, EcsDependsOn, depends_on);
    }
}

void FlecsPipelineImport(
    ecs_world_t *world)
{
    ECS_MODULE(world, FlecsPipeline);
    ECS_IMPORT(world, FlecsSystem);

    ecs_set_name_prefix(world, "Ecs");

    flecs_bootstrap_component(world, EcsPipeline);
    flecs_bootstrap_tag(world, EcsPhase);

    /* Create anonymous phases to which the builtin phases will have DependsOn 
     * relationships. This ensures that, for example, EcsOnUpdate doesn't have a
     * direct DependsOn relationship on EcsPreUpdate, which ensures that when
     * the EcsPreUpdate phase is disabled, EcsOnUpdate still runs. */
    ecs_entity_t phase_0 = ecs_new(world, 0);
    ecs_entity_t phase_1 = ecs_new_w_pair(world, EcsDependsOn, phase_0);
    ecs_entity_t phase_2 = ecs_new_w_pair(world, EcsDependsOn, phase_1);
    ecs_entity_t phase_3 = ecs_new_w_pair(world, EcsDependsOn, phase_2);
    ecs_entity_t phase_4 = ecs_new_w_pair(world, EcsDependsOn, phase_3);
    ecs_entity_t phase_5 = ecs_new_w_pair(world, EcsDependsOn, phase_4);
    ecs_entity_t phase_6 = ecs_new_w_pair(world, EcsDependsOn, phase_5);
    ecs_entity_t phase_7 = ecs_new_w_pair(world, EcsDependsOn, phase_6);
    ecs_entity_t phase_8 = ecs_new_w_pair(world, EcsDependsOn, phase_7);

    flecs_bootstrap_phase(world, EcsOnStart,   0);
    flecs_bootstrap_phase(world, EcsPreFrame,   0);
    flecs_bootstrap_phase(world, EcsOnLoad,     phase_0);
    flecs_bootstrap_phase(world, EcsPostLoad,   phase_1);
    flecs_bootstrap_phase(world, EcsPreUpdate,  phase_2);
    flecs_bootstrap_phase(world, EcsOnUpdate,   phase_3);
    flecs_bootstrap_phase(world, EcsOnValidate, phase_4);
    flecs_bootstrap_phase(world, EcsPostUpdate, phase_5);
    flecs_bootstrap_phase(world, EcsPreStore,   phase_6);
    flecs_bootstrap_phase(world, EcsOnStore,    phase_7);
    flecs_bootstrap_phase(world, EcsPostFrame,  phase_8);

    ecs_set_hooks(world, EcsPipeline, {
        .ctor = ecs_default_ctor,
        .dtor = ecs_dtor(EcsPipeline),
        .move = ecs_move(EcsPipeline)
    });

    world->pipeline = ecs_pipeline(world, {
        .entity = ecs_entity(world, { .name = "BuiltinPipeline" }),
        .query = {
            .filter.terms = {
                { .id = EcsSystem },
                { .id = EcsPhase, .src.flags = EcsCascade, .src.trav = EcsDependsOn },
                { .id = ecs_dependson(EcsOnStart), .src.trav = EcsDependsOn, .oper = EcsNot },
                { .id = EcsDisabled, .src.flags = EcsUp, .src.trav = EcsDependsOn, .oper = EcsNot },
                { .id = EcsDisabled, .src.flags = EcsUp, .src.trav = EcsChildOf, .oper = EcsNot }
            },
            .order_by = flecs_entity_compare
        }
    });

    /* Cleanup thread administration when world is destroyed */
    ecs_atfini(world, FlecsPipelineFini, NULL);
}

#endif

/**
 * @file addons/monitor.c
 * @brief Monitor addon.
 */


#ifdef FLECS_MONITOR

ECS_COMPONENT_DECLARE(FlecsMonitor);
ECS_COMPONENT_DECLARE(EcsWorldStats);
ECS_COMPONENT_DECLARE(EcsPipelineStats);

ecs_entity_t EcsPeriod1s = 0;
ecs_entity_t EcsPeriod1m = 0;
ecs_entity_t EcsPeriod1h = 0;
ecs_entity_t EcsPeriod1d = 0;
ecs_entity_t EcsPeriod1w = 0;

static int32_t flecs_day_interval_count = 24;
static int32_t flecs_week_interval_count = 168;

static
ECS_COPY(EcsPipelineStats, dst, src, {
    (void)dst;
    (void)src;
    ecs_abort(ECS_INVALID_OPERATION, "cannot copy pipeline stats component");
})

static
ECS_MOVE(EcsPipelineStats, dst, src, {
    ecs_os_memcpy_t(dst, src, EcsPipelineStats);
    ecs_os_zeromem(src);
})

static
ECS_DTOR(EcsPipelineStats, ptr, {
    ecs_pipeline_stats_fini(&ptr->stats);
})

static
void MonitorStats(ecs_iter_t *it) {
    ecs_world_t *world = it->real_world;

    EcsStatsHeader *hdr = ecs_field_w_size(it, 0, 1);
    ecs_id_t kind = ecs_pair_first(it->world, ecs_field_id(it, 1));
    void *stats = ECS_OFFSET_T(hdr, EcsStatsHeader);

    ecs_ftime_t elapsed = hdr->elapsed;
    hdr->elapsed += it->delta_time;

    int32_t t_last = (int32_t)(elapsed * 60);
    int32_t t_next = (int32_t)(hdr->elapsed * 60);
    int32_t i, dif = t_last - t_next;

    ecs_world_stats_t last_world = {0};
    ecs_pipeline_stats_t last_pipeline = {0};
    void *last = NULL;

    if (!dif) {
        /* Copy last value so we can pass it to reduce_last */
        if (kind == ecs_id(EcsWorldStats)) {
            last = &last_world;
            ecs_world_stats_copy_last(&last_world, stats);
        } else if (kind == ecs_id(EcsPipelineStats)) {
            last = &last_pipeline;
            ecs_pipeline_stats_copy_last(&last_pipeline, stats);
        }
    }

    if (kind == ecs_id(EcsWorldStats)) {
        ecs_world_stats_get(world, stats);
    } else if (kind == ecs_id(EcsPipelineStats)) {
        ecs_pipeline_stats_get(world, ecs_get_pipeline(world), stats);
    }

    if (!dif) {
        /* Still in same interval, combine with last measurement */
        hdr->reduce_count ++;
        if (kind == ecs_id(EcsWorldStats)) {
            ecs_world_stats_reduce_last(stats, last, hdr->reduce_count);
        } else if (kind == ecs_id(EcsPipelineStats)) {
            ecs_pipeline_stats_reduce_last(stats, last, hdr->reduce_count);
        }
    } else if (dif > 1) {
        /* More than 16ms has passed, backfill */
        for (i = 1; i < dif; i ++) {
            if (kind == ecs_id(EcsWorldStats)) {
                ecs_world_stats_repeat_last(stats);
            } else if (kind == ecs_id(EcsPipelineStats)) {
                ecs_world_stats_repeat_last(stats);
            }
        }
        hdr->reduce_count = 0;
    }

    if (last && kind == ecs_id(EcsPipelineStats)) {
        ecs_pipeline_stats_fini(last);
    }
}

static
void ReduceStats(ecs_iter_t *it) {
    void *dst = ecs_field_w_size(it, 0, 1);
    void *src = ecs_field_w_size(it, 0, 2);

    ecs_id_t kind = ecs_pair_first(it->world, ecs_field_id(it, 1));

    dst = ECS_OFFSET_T(dst, EcsStatsHeader);
    src = ECS_OFFSET_T(src, EcsStatsHeader);

    if (kind == ecs_id(EcsWorldStats)) {
        ecs_world_stats_reduce(dst, src);
    } else if (kind == ecs_id(EcsPipelineStats)) {
        ecs_pipeline_stats_reduce(dst, src);
    }
}

static
void AggregateStats(ecs_iter_t *it) {
    int32_t interval = *(int32_t*)it->ctx;

    EcsStatsHeader *dst_hdr = ecs_field_w_size(it, 0, 1);
    EcsStatsHeader *src_hdr = ecs_field_w_size(it, 0, 2);

    void *dst = ECS_OFFSET_T(dst_hdr, EcsStatsHeader);
    void *src = ECS_OFFSET_T(src_hdr, EcsStatsHeader);

    ecs_id_t kind = ecs_pair_first(it->world, ecs_field_id(it, 1));

    ecs_world_stats_t last_world = {0};
    ecs_pipeline_stats_t last_pipeline = {0};
    void *last = NULL;

    if (dst_hdr->reduce_count != 0) {
        /* Copy last value so we can pass it to reduce_last */
        if (kind == ecs_id(EcsWorldStats)) {
            last_world.t = 0;
            ecs_world_stats_copy_last(&last_world, dst);
            last = &last_world;
        } else if (kind == ecs_id(EcsPipelineStats)) {
            last_pipeline.t = 0;
            ecs_pipeline_stats_copy_last(&last_pipeline, dst);
            last = &last_pipeline;
        }
    }

    /* Reduce from minutes to the current day */
    if (kind == ecs_id(EcsWorldStats)) {
        ecs_world_stats_reduce(dst, src);
    } else if (kind == ecs_id(EcsPipelineStats)) {
        ecs_pipeline_stats_reduce(dst, src);
    }

    if (dst_hdr->reduce_count != 0) {
        if (kind == ecs_id(EcsWorldStats)) {
            ecs_world_stats_reduce_last(dst, last, dst_hdr->reduce_count);
        } else if (kind == ecs_id(EcsPipelineStats)) {
            ecs_pipeline_stats_reduce_last(dst, last, dst_hdr->reduce_count);
        }
    }

    /* A day has 60 24 minute intervals */
    dst_hdr->reduce_count ++;
    if (dst_hdr->reduce_count >= interval) {
        dst_hdr->reduce_count = 0;
    }

    if (last && kind == ecs_id(EcsPipelineStats)) {
        ecs_pipeline_stats_fini(last);
    }
}

static
void flecs_stats_monitor_import(
    ecs_world_t *world,
    ecs_id_t kind,
    size_t size)
{
    ecs_entity_t prev = ecs_set_scope(world, kind);

    // Called each frame, collects 60 measurements per second
    ecs_system(world, {
        .entity = ecs_entity(world, { .name = "Monitor1s", .add = {ecs_dependson(EcsPreFrame)} }),
        .query.filter.terms = {{
            .id = ecs_pair(kind, EcsPeriod1s),
            .src.id = EcsWorld 
        }},
        .callback = MonitorStats
    });

    // Called each second, reduces into 60 measurements per minute
    ecs_entity_t mw1m = ecs_system(world, {
        .entity = ecs_entity(world, { .name = "Monitor1m", .add = {ecs_dependson(EcsPreFrame)} }),
        .query.filter.terms = {{
            .id = ecs_pair(kind, EcsPeriod1m),
            .src.id = EcsWorld 
        }, {
            .id = ecs_pair(kind, EcsPeriod1s),
            .src.id = EcsWorld 
        }},
        .callback = ReduceStats,
        .interval = 1.0
    });

    // Called each minute, reduces into 60 measurements per hour
    ecs_system(world, {
        .entity = ecs_entity(world, { .name = "Monitor1h", .add = {ecs_dependson(EcsPreFrame)} }),
        .query.filter.terms = {{
            .id = ecs_pair(kind, EcsPeriod1h),
            .src.id = EcsWorld 
        }, {
            .id = ecs_pair(kind, EcsPeriod1m),
            .src.id = EcsWorld 
        }},
        .callback = ReduceStats,
        .rate = 60,
        .tick_source = mw1m
    });

    // Called each minute, reduces into 60 measurements per day
    ecs_system(world, {
        .entity = ecs_entity(world, { .name = "Monitor1d", .add = {ecs_dependson(EcsPreFrame)} }),
        .query.filter.terms = {{
            .id = ecs_pair(kind, EcsPeriod1d),
            .src.id = EcsWorld 
        }, {
            .id = ecs_pair(kind, EcsPeriod1m),
            .src.id = EcsWorld 
        }},
        .callback = AggregateStats,
        .rate = 60,
        .tick_source = mw1m,
        .ctx = &flecs_day_interval_count
    });

    // Called each hour, reduces into 60 measurements per week
    ecs_system(world, {
        .entity = ecs_entity(world, { .name = "Monitor1w", .add = {ecs_dependson(EcsPreFrame)} }),
        .query.filter.terms = {{
            .id = ecs_pair(kind, EcsPeriod1w),
            .src.id = EcsWorld 
        }, {
            .id = ecs_pair(kind, EcsPeriod1h),
            .src.id = EcsWorld 
        }},
        .callback = AggregateStats,
        .rate = 60,
        .tick_source = mw1m,
        .ctx = &flecs_week_interval_count
    });

    ecs_set_scope(world, prev);

    ecs_set_id(world, EcsWorld, ecs_pair(kind, EcsPeriod1s), size, NULL);
    ecs_set_id(world, EcsWorld, ecs_pair(kind, EcsPeriod1m), size, NULL);
    ecs_set_id(world, EcsWorld, ecs_pair(kind, EcsPeriod1h), size, NULL);
    ecs_set_id(world, EcsWorld, ecs_pair(kind, EcsPeriod1d), size, NULL);
    ecs_set_id(world, EcsWorld, ecs_pair(kind, EcsPeriod1w), size, NULL);
}

static
void flecs_world_monitor_import(
    ecs_world_t *world)
{
    ECS_COMPONENT_DEFINE(world, EcsWorldStats);

    flecs_stats_monitor_import(world, ecs_id(EcsWorldStats), 
        sizeof(EcsWorldStats));
}

static
void flecs_pipeline_monitor_import(
    ecs_world_t *world)
{
    ECS_COMPONENT_DEFINE(world, EcsPipelineStats);

    ecs_set_hooks(world, EcsPipelineStats, {
        .ctor = ecs_default_ctor,
        .copy = ecs_copy(EcsPipelineStats),
        .move = ecs_move(EcsPipelineStats),
        .dtor = ecs_dtor(EcsPipelineStats)
    });

    flecs_stats_monitor_import(world, ecs_id(EcsPipelineStats),
        sizeof(EcsPipelineStats));
}

void FlecsMonitorImport(
    ecs_world_t *world)
{
    ECS_MODULE_DEFINE(world, FlecsMonitor);
    ECS_IMPORT(world, FlecsPipeline);
    ECS_IMPORT(world, FlecsTimer);

    ecs_set_name_prefix(world, "Ecs");

    EcsPeriod1s = ecs_new_entity(world, "EcsPeriod1s");
    EcsPeriod1m = ecs_new_entity(world, "EcsPeriod1m");
    EcsPeriod1h = ecs_new_entity(world, "EcsPeriod1h");
    EcsPeriod1d = ecs_new_entity(world, "EcsPeriod1d");
    EcsPeriod1w = ecs_new_entity(world, "EcsPeriod1w");

    flecs_world_monitor_import(world);
    flecs_pipeline_monitor_import(world);
    
    if (ecs_os_has_time()) {
        ecs_measure_frame_time(world, true);
        ecs_measure_system_time(world, true);
    }
}

#endif

/**
 * @file addons/timer.c
 * @brief Timer addon.
 */


#ifdef FLECS_TIMER

static
void AddTickSource(ecs_iter_t *it) {
    int32_t i;
    for (i = 0; i < it->count; i ++) {
        ecs_set(it->world, it->entities[i], EcsTickSource, {0});
    }
}

static
void ProgressTimers(ecs_iter_t *it) {
    EcsTimer *timer = ecs_field(it, EcsTimer, 1);
    EcsTickSource *tick_source = ecs_field(it, EcsTickSource, 2);

    ecs_assert(timer != NULL, ECS_INTERNAL_ERROR, NULL);

    int i;
    for (i = 0; i < it->count; i ++) {
        tick_source[i].tick = false;

        if (!timer[i].active) {
            continue;
        }

        const ecs_world_info_t *info = ecs_get_world_info(it->world);
        ecs_ftime_t time_elapsed = timer[i].time + info->delta_time_raw;
        ecs_ftime_t timeout = timer[i].timeout;
        
        if (time_elapsed >= timeout) {
            ecs_ftime_t t = time_elapsed - timeout;
            if (t > timeout) {
                t = 0;
            }

            timer[i].time = t; /* Initialize with remainder */
            tick_source[i].tick = true;
            tick_source[i].time_elapsed = time_elapsed - timer[i].overshoot;
            timer[i].overshoot = t;

            if (timer[i].single_shot) {
                timer[i].active = false;
            }
        } else {
            timer[i].time = time_elapsed;
        }  
    }
}

static
void ProgressRateFilters(ecs_iter_t *it) {
    EcsRateFilter *filter = ecs_field(it, EcsRateFilter, 1);
    EcsTickSource *tick_dst = ecs_field(it, EcsTickSource, 2);

    int i;
    for (i = 0; i < it->count; i ++) {
        ecs_entity_t src = filter[i].src;
        bool inc = false;

        filter[i].time_elapsed += it->delta_time;

        if (src) {
            const EcsTickSource *tick_src = ecs_get(it->world, src, EcsTickSource);
            if (tick_src) {
                inc = tick_src->tick;
            } else {
                inc = true;
            }
        } else {
            inc = true;
        }

        if (inc) {
            filter[i].tick_count ++;
            bool triggered = !(filter[i].tick_count % filter[i].rate);
            tick_dst[i].tick = triggered;
            tick_dst[i].time_elapsed = filter[i].time_elapsed;

            if (triggered) {
                filter[i].time_elapsed = 0;
            }            
        } else {
            tick_dst[i].tick = false;
        }
    }
}

static
void ProgressTickSource(ecs_iter_t *it) {
    EcsTickSource *tick_src = ecs_field(it, EcsTickSource, 1);

    /* If tick source has no filters, tick unconditionally */
    int i;
    for (i = 0; i < it->count; i ++) {
        tick_src[i].tick = true;
        tick_src[i].time_elapsed = it->delta_time;
    }
}

ecs_entity_t ecs_set_timeout(
    ecs_world_t *world,
    ecs_entity_t timer,
    ecs_ftime_t timeout)
{
    ecs_check(world != NULL, ECS_INVALID_PARAMETER, NULL);

    timer = ecs_set(world, timer, EcsTimer, {
        .timeout = timeout,
        .single_shot = true,
        .active = true
    });

    ecs_system_t *system_data = ecs_poly_get(world, timer, ecs_system_t);
    if (system_data) {
        system_data->tick_source = timer;
    }

error:
    return timer;
}

ecs_ftime_t ecs_get_timeout(
    const ecs_world_t *world,
    ecs_entity_t timer)
{
    ecs_check(world != NULL, ECS_INVALID_PARAMETER, NULL);
    ecs_check(timer != 0, ECS_INVALID_PARAMETER, NULL);

    const EcsTimer *value = ecs_get(world, timer, EcsTimer);
    if (value) {
        return value->timeout;
    }
error:
    return 0;
}

ecs_entity_t ecs_set_interval(
    ecs_world_t *world,
    ecs_entity_t timer,
    ecs_ftime_t interval)
{
    ecs_check(world != NULL, ECS_INVALID_PARAMETER, NULL);

    timer = ecs_set(world, timer, EcsTimer, {
        .timeout = interval,
        .active = true
    });

    ecs_system_t *system_data = ecs_poly_get(world, timer, ecs_system_t);
    if (system_data) {
        system_data->tick_source = timer;
    }
error:
    return timer;  
}

ecs_ftime_t ecs_get_interval(
    const ecs_world_t *world,
    ecs_entity_t timer)
{
    ecs_check(world != NULL, ECS_INVALID_PARAMETER, NULL);

    if (!timer) {
        return 0;
    }

    const EcsTimer *value = ecs_get(world, timer, EcsTimer);
    if (value) {
        return value->timeout;
    }
error:
    return 0;
}

void ecs_start_timer(
    ecs_world_t *world,
    ecs_entity_t timer)
{
    EcsTimer *ptr = ecs_get_mut(world, timer, EcsTimer);
    ecs_check(ptr != NULL, ECS_INVALID_PARAMETER, NULL);
    ptr->active = true;
    ptr->time = 0;
error:
    return;
}

void ecs_stop_timer(
    ecs_world_t *world,
    ecs_entity_t timer)
{
    EcsTimer *ptr = ecs_get_mut(world, timer, EcsTimer);
    ecs_check(ptr != NULL, ECS_INVALID_PARAMETER, NULL);
    ptr->active = false;
error:
    return;
}

ecs_entity_t ecs_set_rate(
    ecs_world_t *world,
    ecs_entity_t filter,
    int32_t rate,
    ecs_entity_t source)
{
    ecs_check(world != NULL, ECS_INVALID_PARAMETER, NULL);

    filter = ecs_set(world, filter, EcsRateFilter, {
        .rate = rate,
        .src = source
    });

    ecs_system_t *system_data = ecs_poly_get(world, filter, ecs_system_t);
    if (system_data) {
        system_data->tick_source = filter;
    }  

error:
    return filter;     
}

void ecs_set_tick_source(
    ecs_world_t *world,
    ecs_entity_t system,
    ecs_entity_t tick_source)
{
    ecs_check(world != NULL, ECS_INVALID_PARAMETER, NULL);
    ecs_check(system != 0, ECS_INVALID_PARAMETER, NULL);
    ecs_check(tick_source != 0, ECS_INVALID_PARAMETER, NULL);

    ecs_system_t *system_data = ecs_poly_get(world, system, ecs_system_t);
    ecs_check(system_data != NULL, ECS_INVALID_PARAMETER, NULL);

    system_data->tick_source = tick_source;
error:
    return;
}

void FlecsTimerImport(
    ecs_world_t *world)
{    
    ECS_MODULE(world, FlecsTimer);

    ECS_IMPORT(world, FlecsPipeline);

    ecs_set_name_prefix(world, "Ecs");

    flecs_bootstrap_component(world, EcsTimer);
    flecs_bootstrap_component(world, EcsRateFilter);

    /* Add EcsTickSource to timers and rate filters */
    ecs_system(world, {
        .entity = ecs_entity(world, {.name = "AddTickSource", .add = { ecs_dependson(EcsPreFrame) }}),
        .query.filter.terms = {
            { .id = ecs_id(EcsTimer), .oper = EcsOr, .inout = EcsIn },
            { .id = ecs_id(EcsRateFilter), .oper = EcsAnd, .inout = EcsIn },
            { .id = ecs_id(EcsTickSource), .oper = EcsNot, .inout = EcsOut}
        },
        .callback = AddTickSource
    });

    /* Timer handling */
    ecs_system(world, {
        .entity = ecs_entity(world, {.name = "ProgressTimers", .add = { ecs_dependson(EcsPreFrame)}}),
        .query.filter.terms = {
            { .id = ecs_id(EcsTimer) },
            { .id = ecs_id(EcsTickSource) }
        },
        .callback = ProgressTimers
    });

    /* Rate filter handling */
    ecs_system(world, {
        .entity = ecs_entity(world, {.name = "ProgressRateFilters", .add = { ecs_dependson(EcsPreFrame)}}),
        .query.filter.terms = {
            { .id = ecs_id(EcsRateFilter), .inout = EcsIn },
            { .id = ecs_id(EcsTickSource), .inout = EcsOut }
        },
        .callback = ProgressRateFilters
    });

    /* TickSource without a timer or rate filter just increases each frame */
    ecs_system(world, {
        .entity = ecs_entity(world, { .name = "ProgressTickSource", .add = { ecs_dependson(EcsPreFrame)}}),
        .query.filter.terms = {
            { .id = ecs_id(EcsTickSource), .inout = EcsOut },
            { .id = ecs_id(EcsRateFilter), .oper = EcsNot },
            { .id = ecs_id(EcsTimer), .oper = EcsNot }
        },
        .callback = ProgressTickSource
    });
}

#endif

/**
 * @file addons/flecs_cpp.c
 * @brief Utilities for C++ addon.
 */

#include <ctype.h>

/* Utilities for C++ API */

#ifdef FLECS_CPP

/* Convert compiler-specific typenames extracted from __PRETTY_FUNCTION__ to
 * a uniform identifier */

#define ECS_CONST_PREFIX "const "
#define ECS_STRUCT_PREFIX "struct "
#define ECS_CLASS_PREFIX "class "
#define ECS_ENUM_PREFIX "enum "

#define ECS_CONST_LEN (-1 + (ecs_size_t)sizeof(ECS_CONST_PREFIX))
#define ECS_STRUCT_LEN (-1 + (ecs_size_t)sizeof(ECS_STRUCT_PREFIX))
#define ECS_CLASS_LEN (-1 + (ecs_size_t)sizeof(ECS_CLASS_PREFIX))
#define ECS_ENUM_LEN (-1 + (ecs_size_t)sizeof(ECS_ENUM_PREFIX))

static
ecs_size_t ecs_cpp_strip_prefix(
    char *typeName,
    ecs_size_t len,
    const char *prefix,
    ecs_size_t prefix_len)
{
    if ((len > prefix_len) && !ecs_os_strncmp(typeName, prefix, prefix_len)) {
        ecs_os_memmove(typeName, typeName + prefix_len, len - prefix_len);
        typeName[len - prefix_len] = '\0';
        len -= prefix_len;
    }
    return len;
}

static 
void ecs_cpp_trim_type_name(
    char *typeName) 
{
    ecs_size_t len = ecs_os_strlen(typeName);

    len = ecs_cpp_strip_prefix(typeName, len, ECS_CONST_PREFIX, ECS_CONST_LEN);
    len = ecs_cpp_strip_prefix(typeName, len, ECS_STRUCT_PREFIX, ECS_STRUCT_LEN);
    len = ecs_cpp_strip_prefix(typeName, len, ECS_CLASS_PREFIX, ECS_CLASS_LEN);
    len = ecs_cpp_strip_prefix(typeName, len, ECS_ENUM_PREFIX, ECS_ENUM_LEN);

    while (typeName[len - 1] == ' ' ||
            typeName[len - 1] == '&' ||
            typeName[len - 1] == '*') 
    {
        len --;
        typeName[len] = '\0';
    }

    /* Remove const at end of string */
    if (len > ECS_CONST_LEN) {
        if (!ecs_os_strncmp(&typeName[len - ECS_CONST_LEN], " const", ECS_CONST_LEN)) {
            typeName[len - ECS_CONST_LEN] = '\0';
        }
        len -= ECS_CONST_LEN;
    }

    /* Check if there are any remaining "struct " strings, which can happen
     * if this is a template type on msvc. */
    if (len > ECS_STRUCT_LEN) {
        char *ptr = typeName;
        while ((ptr = strstr(ptr + 1, ECS_STRUCT_PREFIX)) != 0) {
            /* Make sure we're not matched with part of a longer identifier
             * that contains 'struct' */
            if (ptr[-1] == '<' || ptr[-1] == ',' || isspace(ptr[-1])) {
                ecs_os_memmove(ptr, ptr + ECS_STRUCT_LEN, 
                    ecs_os_strlen(ptr + ECS_STRUCT_LEN) + 1);
                len -= ECS_STRUCT_LEN;
            }
        }
    }
}

char* ecs_cpp_get_type_name(
    char *type_name, 
    const char *func_name,
    size_t len,
    size_t front_len)
{
    memcpy(type_name, func_name + front_len, len);
    type_name[len] = '\0';
    ecs_cpp_trim_type_name(type_name);
    return type_name;
}

char* ecs_cpp_get_symbol_name(
    char *symbol_name,
    const char *type_name,
    size_t len)
{
    // Symbol is same as name, but with '::' replaced with '.'
    ecs_os_strcpy(symbol_name, type_name);

    char *ptr;
    size_t i;
    for (i = 0, ptr = symbol_name; i < len && *ptr; i ++, ptr ++) {
        if (*ptr == ':') {
            symbol_name[i] = '.';
            ptr ++;
        } else {
            symbol_name[i] = *ptr;
        }
    }

    symbol_name[i] = '\0';

    return symbol_name;
}

static
const char* flecs_cpp_func_rchr(
    const char *func_name,
    ecs_size_t func_name_len,
    ecs_size_t func_back_len,
    char ch)
{
    const char *r = strrchr(func_name, ch);
    if ((r - func_name) >= (func_name_len - flecs_uto(ecs_size_t, func_back_len))) {
        return NULL;
    }
    return r;
}

static
const char* flecs_cpp_func_max(
    const char *a,
    const char *b)
{
    if (a > b) return a;
    return b;
}

char* ecs_cpp_get_constant_name(
    char *constant_name,
    const char *func_name,
    size_t func_name_len,
    size_t func_back_len)
{
    ecs_size_t f_len = flecs_uto(ecs_size_t, func_name_len);
    ecs_size_t fb_len = flecs_uto(ecs_size_t, func_back_len);
    const char *start = flecs_cpp_func_rchr(func_name, f_len, fb_len, ' ');
    start = flecs_cpp_func_max(start, flecs_cpp_func_rchr(
        func_name, f_len, fb_len, ')'));
    start = flecs_cpp_func_max(start, flecs_cpp_func_rchr(
        func_name, f_len, fb_len, ':'));
    start = flecs_cpp_func_max(start, flecs_cpp_func_rchr(
        func_name, f_len, fb_len, ','));
    ecs_assert(start != NULL, ECS_INVALID_PARAMETER, func_name);
    start ++;
    
    ecs_size_t len = flecs_uto(ecs_size_t, 
        (f_len - (start - func_name) - fb_len));
    ecs_os_memcpy_n(constant_name, start, char, len);
    constant_name[len] = '\0';
    return constant_name;
}

// Names returned from the name_helper class do not start with ::
// but are relative to the root. If the namespace of the type
// overlaps with the namespace of the current module, strip it from
// the implicit identifier.
// This allows for registration of component types that are not in the 
// module namespace to still be registered under the module scope.
const char* ecs_cpp_trim_module(
    ecs_world_t *world,
    const char *type_name)
{
    ecs_entity_t scope = ecs_get_scope(world);
    if (!scope) {
        return type_name;
    }

    char *path = ecs_get_path_w_sep(world, 0, scope, "::", NULL);
    if (path) {
        const char *ptr = strrchr(type_name, ':');
        ecs_assert(ptr != type_name, ECS_INTERNAL_ERROR, NULL);
        if (ptr) {
            ptr --;
            ecs_assert(ptr[0] == ':', ECS_INTERNAL_ERROR, NULL);
            ecs_size_t name_path_len = (ecs_size_t)(ptr - type_name);
            if (name_path_len <= ecs_os_strlen(path)) {
                if (!ecs_os_strncmp(type_name, path, name_path_len)) {
                    type_name = &type_name[name_path_len + 2];
                }
            }
        }
    }
    ecs_os_free(path);

    return type_name;
}

// Validate registered component
void ecs_cpp_component_validate(
    ecs_world_t *world,
    ecs_entity_t id,
    const char *name,
    const char *symbol,
    size_t size,
    size_t alignment,
    bool implicit_name)
{
    /* If entity has a name check if it matches */
    if (ecs_is_valid(world, id) && ecs_get_name(world, id) != NULL) {
        if (!implicit_name && id >= EcsFirstUserComponentId) {
#ifndef FLECS_NDEBUG
            char *path = ecs_get_path_w_sep(
                world, 0, id, "::", NULL);
            if (ecs_os_strcmp(path, name)) {
                ecs_abort(ECS_INCONSISTENT_NAME,
                    "component '%s' already registered with name '%s'",
                    name, path);
            }
            ecs_os_free(path);
#endif
        }

        if (symbol) {
            const char *existing_symbol = ecs_get_symbol(world, id);
            if (existing_symbol) {
                if (ecs_os_strcmp(symbol, existing_symbol)) {
                    ecs_abort(ECS_INCONSISTENT_NAME,
                        "component '%s' with symbol '%s' already registered with symbol '%s'",
                        name, symbol, existing_symbol);
                }
            }
        }
    } else {
        /* Ensure that the entity id valid */
        if (!ecs_is_alive(world, id)) {
            ecs_ensure(world, id);
        }

        /* Register name with entity, so that when the entity is created the
        * correct id will be resolved from the name. Only do this when the
        * entity is empty. */
        ecs_add_path_w_sep(world, id, 0, name, "::", "::");
    }

    /* If a component was already registered with this id but with a 
     * different size, the ecs_component_init function will fail. */

    /* We need to explicitly call ecs_component_init here again. Even though
     * the component was already registered, it may have been registered
     * with a different world. This ensures that the component is registered
     * with the same id for the current world. 
     * If the component was registered already, nothing will change. */
    ecs_entity_t ent = ecs_component_init(world, &(ecs_component_desc_t){
        .entity = id,
        .type.size = flecs_uto(int32_t, size),
        .type.alignment = flecs_uto(int32_t, alignment)
    });
    (void)ent;
    ecs_assert(ent == id, ECS_INTERNAL_ERROR, NULL);
}

ecs_entity_t ecs_cpp_component_register(
    ecs_world_t *world,
    ecs_entity_t id,
    const char *name,
    const char *symbol,
    ecs_size_t size,
    ecs_size_t alignment,
    bool implicit_name,
    bool *existing_out)
{
    (void)size;
    (void)alignment;

    /* If the component is not yet registered, ensure no other component
     * or entity has been registered with this name. Ensure component is 
     * looked up from root. */
    bool existing = false;
    ecs_entity_t prev_scope = ecs_set_scope(world, 0);
    ecs_entity_t ent;
    if (id) {
        ent = id;
    } else {
        ent = ecs_lookup_path_w_sep(world, 0, name, "::", "::", false);
        existing = ent != 0;
    }
    ecs_set_scope(world, prev_scope);

    /* If entity exists, compare symbol name to ensure that the component
     * we are trying to register under this name is the same */
    if (ent) {
        const EcsComponent *component = ecs_get(world, ent, EcsComponent);
        if (component != NULL) {
            const char *sym = ecs_get_symbol(world, ent);
            if (sym && ecs_os_strcmp(sym, symbol)) {
                /* Application is trying to register a type with an entity that
                 * was already associated with another type. In most cases this
                 * is an error, with the exception of a scenario where the
                 * application is wrapping a C type with a C++ type.
                 * 
                 * In this case the C++ type typically inherits from the C type,
                 * and adds convenience methods to the derived class without
                 * changing anything that would change the size or layout.
                 * 
                 * To meet this condition, the new type must have the same size
                 * and alignment as the existing type, and the name of the type
                 * type must be equal to the registered name (not symbol).
                 * 
                 * The latter ensures that it was the intent of the application
                 * to alias the type, vs. accidentally registering an unrelated
                 * type with the same size/alignment. */
                char *type_path = ecs_get_fullpath(world, ent);
                if (ecs_os_strcmp(type_path, symbol) || 
                    component->size != size || 
                    component->alignment != alignment) 
                {
                    ecs_err(
                        "component with name '%s' is already registered for"\
                        " type '%s' (trying to register for type '%s')",
                            name, sym, symbol);
                    ecs_abort(ECS_NAME_IN_USE, NULL);
                }
                ecs_os_free(type_path);
            } else if (!sym) {
                ecs_set_symbol(world, ent, symbol);
            }
        }

    /* If no entity is found, lookup symbol to check if the component was
     * registered under a different name. */
    } else if (!implicit_name) {
        ent = ecs_lookup_symbol(world, symbol, false);
        ecs_assert(ent == 0 || (ent == id), ECS_INCONSISTENT_COMPONENT_ID, symbol);
    }

    if (existing_out) {
        *existing_out = existing;
    }

    return ent;
}

ecs_entity_t ecs_cpp_component_register_explicit(
    ecs_world_t *world,
    ecs_entity_t s_id,
    ecs_entity_t id,
    const char *name,
    const char *type_name,
    const char *symbol,
    size_t size,
    size_t alignment,
    bool is_component,
    bool *existing_out)
{
    char *existing_name = NULL;
    if (existing_out) *existing_out = false;

    // If an explicit id is provided, it is possible that the symbol and
    // name differ from the actual type, as the application may alias
    // one type to another.
    if (!id) {
        if (!name) {
            // If no name was provided first check if a type with the provided
            // symbol was already registered.
            id = ecs_lookup_symbol(world, symbol, false);
            if (id) {
                existing_name = ecs_get_path_w_sep(world, 0, id, "::", "::");
                name = existing_name;
                if (existing_out) *existing_out = true;
            } else {
                // If type is not yet known, derive from type name
                name = ecs_cpp_trim_module(world, type_name);
            }            
        }
    } else {
        // If an explicit id is provided but it has no name, inherit
        // the name from the type.
        if (!ecs_is_valid(world, id) || !ecs_get_name(world, id)) {
            name = ecs_cpp_trim_module(world, type_name);
        }
    }

    ecs_entity_t entity;
    if (is_component || size != 0) {
        entity = ecs_entity(world, {
            .id = s_id,
            .name = name,
            .sep = "::",
            .root_sep = "::",
            .symbol = symbol,
            .use_low_id = true
        });
        ecs_assert(entity != 0, ECS_INVALID_OPERATION, NULL);

        entity = ecs_component_init(world, &(ecs_component_desc_t){
            .entity = entity,
            .type.size = flecs_uto(int32_t, size),
            .type.alignment = flecs_uto(int32_t, alignment)
        });
        ecs_assert(entity != 0, ECS_INVALID_OPERATION, NULL);
    } else {
        entity = ecs_entity(world, {
            .id = s_id,
            .name = name,
            .sep = "::",
            .root_sep = "::",
            .symbol = symbol,
            .use_low_id = true
        });
    }

    ecs_assert(entity != 0, ECS_INTERNAL_ERROR, NULL);
    ecs_assert(!s_id || s_id == entity, ECS_INTERNAL_ERROR, NULL);
    ecs_os_free(existing_name);

    return entity;
}

void ecs_cpp_enum_init(
    ecs_world_t *world,
    ecs_entity_t id)
{
    (void)world;
    (void)id;
#ifdef FLECS_META
    ecs_suspend_readonly_state_t readonly_state;
    world = flecs_suspend_readonly(world, &readonly_state);
    ecs_set(world, id, EcsEnum, {0});
    flecs_resume_readonly(world, &readonly_state);
#endif
}

ecs_entity_t ecs_cpp_enum_constant_register(
    ecs_world_t *world,
    ecs_entity_t parent,
    ecs_entity_t id,
    const char *name,
    int value)
{
    ecs_suspend_readonly_state_t readonly_state;
    world = flecs_suspend_readonly(world, &readonly_state);

    const char *parent_name = ecs_get_name(world, parent);
    ecs_size_t parent_name_len = ecs_os_strlen(parent_name);
    if (!ecs_os_strncmp(name, parent_name, parent_name_len)) {
        name += parent_name_len;
        if (name[0] == '_') {
            name ++;
        }
    }

    ecs_entity_t prev = ecs_set_scope(world, parent);
    id = ecs_entity(world, {
        .id = id,
        .name = name
    });
    ecs_assert(id != 0, ECS_INVALID_OPERATION, name);
    ecs_set_scope(world, prev);

    #ifdef FLECS_DEBUG
    const EcsComponent *cptr = ecs_get(world, parent, EcsComponent);
    ecs_assert(cptr != NULL, ECS_INVALID_PARAMETER, "enum is not a component");
    ecs_assert(cptr->size == ECS_SIZEOF(int32_t), ECS_UNSUPPORTED,
        "enum component must have 32bit size");
    #endif

#ifdef FLECS_META
    ecs_set_id(world, id, ecs_pair(EcsConstant, ecs_id(ecs_i32_t)), 
        sizeof(ecs_i32_t), &value);
#endif

    flecs_resume_readonly(world, &readonly_state);

    ecs_trace("#[green]constant#[reset] %s.%s created with value %d", 
        ecs_get_name(world, parent), name, value);

    return id;
}

static int32_t flecs_reset_count = 0;

int32_t ecs_cpp_reset_count_get(void) {
    return flecs_reset_count;
}

int32_t ecs_cpp_reset_count_inc(void) {
    return ++flecs_reset_count;
}

#endif

/**
 * @file addons/alerts.c
 * @brief Alerts addon.
 */


#ifdef FLECS_ALERTS

ECS_COMPONENT_DECLARE(FlecsAlerts);

typedef struct EcsAlert {
    char *message;
    ecs_map_t instances; /* Active instances for metric */
} EcsAlert;

static
ECS_CTOR(EcsAlert, ptr, {
    ptr->message = NULL;
    ecs_map_init(&ptr->instances, NULL);
})

static
ECS_DTOR(EcsAlert, ptr, {
    ecs_os_free(ptr->message);
    ecs_map_fini(&ptr->instances);
})

static
ECS_MOVE(EcsAlert, dst, src, {
    ecs_os_free(dst->message);
    dst->message = src->message;
    src->message = NULL;

    ecs_map_fini(&dst->instances);
    dst->instances = src->instances;
    src->instances = (ecs_map_t){0};
})

static
ECS_CTOR(EcsAlertsActive, ptr, {
    ecs_map_init(&ptr->alerts, NULL);
})

static
ECS_DTOR(EcsAlertsActive, ptr, {
    ecs_map_fini(&ptr->alerts);
})

static
ECS_MOVE(EcsAlertsActive, dst, src, {
    ecs_map_fini(&dst->alerts);
    dst->alerts = src->alerts;
    src->alerts = (ecs_map_t){0};
})

static
void flecs_alerts_add_alert_to_src(
    ecs_world_t *world,
    ecs_entity_t source,
    ecs_entity_t alert,
    ecs_entity_t alert_instance)
{
    EcsAlertsActive *active = ecs_get_mut(
        world, source, EcsAlertsActive);
    ecs_assert(active != NULL, ECS_INTERNAL_ERROR, NULL);

    ecs_entity_t *ptr = ecs_map_ensure(&active->alerts, alert);
    ecs_assert(ptr != NULL, ECS_INTERNAL_ERROR, NULL);
    ptr[0] = alert_instance;
    ecs_modified(world, source, EcsAlertsActive);
}

static
void flecs_alerts_remove_alert_from_src(
    ecs_world_t *world,
    ecs_entity_t source,
    ecs_entity_t alert)
{
    EcsAlertsActive *active = ecs_get_mut(
        world, source, EcsAlertsActive);
    ecs_assert(active != NULL, ECS_INTERNAL_ERROR, NULL);
    ecs_map_remove(&active->alerts, alert);

    if (!ecs_map_count(&active->alerts)) {
        ecs_remove(world, source, EcsAlertsActive);
    } else {
        ecs_modified(world, source, EcsAlertsActive);
    }
}

static
void MonitorAlerts(ecs_iter_t *it) {
    ecs_world_t *world = it->real_world;
    EcsAlert *alert = ecs_field(it, EcsAlert, 1);
    EcsPoly *poly = ecs_field(it, EcsPoly, 2);

    int32_t i, count = it->count;
    for (i = 0; i < count; i ++) {
        ecs_entity_t a = it->entities[i]; /* Alert entity */
        ecs_rule_t *rule = poly[i].poly;
        ecs_poly_assert(rule, ecs_rule_t);

        ecs_iter_t rit = ecs_rule_iter(world, rule);
        rit.flags |= EcsIterNoData;
        rit.flags |= EcsIterIsInstanced;

        while (ecs_rule_next(&rit)) {
            int32_t j, alert_src_count = rit.count;
            for (j = 0; j < alert_src_count; j ++) {
                ecs_entity_t e = rit.entities[j];
                ecs_entity_t *aptr = ecs_map_ensure(&alert[i].instances, e);
                ecs_assert(aptr != NULL, ECS_INTERNAL_ERROR, NULL);
                if (!aptr[0]) {
                    /* Alert does not yet exist for entity */
                    ecs_entity_t ai = ecs_new_w_pair(world, EcsChildOf, a);
                    ecs_set(world, ai, EcsAlertInstance, { .message = NULL });
                    ecs_set(world, ai, EcsMetricSource, { .entity = e });
                    ecs_set(world, ai, EcsMetricValue, { .value = 0 });
                    ecs_doc_set_color(world, ai, "#b5494b");
                    ecs_defer_suspend(it->world);
                    flecs_alerts_add_alert_to_src(world, e, a, ai);
                    ecs_defer_resume(it->world);
                    aptr[0] = ai;
                }
            }
        }
    }
}

static
void MonitorAlertInstances(ecs_iter_t *it) {
    ecs_world_t *world = it->real_world;
    EcsAlertInstance *alert_instance = ecs_field(it, EcsAlertInstance, 1);
    EcsMetricSource *source = ecs_field(it, EcsMetricSource, 2);
    EcsMetricValue *value = ecs_field(it, EcsMetricValue, 3);

    /* Get alert component from alert instance parent (the alert) */
    ecs_id_t childof_pair;
    if (ecs_search(world, it->table, ecs_childof(EcsWildcard), &childof_pair) == -1) {
        ecs_err("alert instances must be a child of an alert");
        return;
    }
    ecs_entity_t parent = ecs_pair_second(world, childof_pair);
    ecs_assert(parent != 0, ECS_INTERNAL_ERROR, NULL);
    ecs_assert(ecs_has(world, parent, EcsAlert), ECS_INVALID_OPERATION,
        "alert entity does not have Alert component");
    EcsAlert *alert = ecs_get_mut(world, parent, EcsAlert);
    const EcsPoly *poly = ecs_get_pair(world, parent, EcsPoly, EcsQuery);
    ecs_assert(poly != NULL, ECS_INVALID_OPERATION, 
        "alert entity does not have (Poly, Query) component");
    ecs_rule_t *rule = poly->poly;
    ecs_poly_assert(rule, ecs_rule_t);

    ecs_vars_t vars = {0};
    ecs_vars_init(world, &vars);

    int32_t i, count = it->count;
    for (i = 0; i < count; i ++) {
        ecs_entity_t ai = it->entities[i];
        ecs_entity_t e = source[i].entity;

        value[i].value += (double)it->delta_system_time;

        /* Check if alert instance still matches rule */
        ecs_iter_t rit = ecs_rule_iter(world, rule);
        rit.flags |= EcsIterNoData;
        rit.flags |= EcsIterIsInstanced;
        ecs_iter_set_var(&rit, 0, e);

        if (ecs_rule_next(&rit)) {
            bool generate_message = alert->message;
            if (generate_message) {
                if (alert_instance[i].message) {
                    /* If a message was already generated, only regenerate if
                     * rule has multiple variables. Variable values could have 
                     * changed, this ensures the message remains up to date. */
                    generate_message = rit.variable_count > 1;
                }
            }

            if (generate_message) {
                if (alert_instance[i].message) {
                    ecs_os_free(alert_instance[i].message);
                }

                ecs_iter_to_vars(&rit, &vars, 0);
                alert_instance[i].message = ecs_interpolate_string(
                    world, alert->message, &vars);
            }

            /* Alert instance still matches rule, keep it alive */
            ecs_iter_fini(&rit);
            continue;
        }

        /* Alert instance no longer matches rule, remove it */
        flecs_alerts_remove_alert_from_src(world, e, parent);
        ecs_map_remove(&alert->instances, e);
        ecs_delete(world, ai);
    }

    ecs_vars_fini(&vars);
}

ecs_entity_t ecs_alert_init(
    ecs_world_t *world,
    const ecs_alert_desc_t *desc)
{
    ecs_poly_assert(world, ecs_world_t);
    ecs_check(desc != NULL, ECS_INVALID_PARAMETER, NULL);
    ecs_check(desc->_canary == 0, ECS_INVALID_PARAMETER, NULL);
    ecs_check(!desc->filter.entity || desc->entity == desc->filter.entity, 
        ECS_INVALID_PARAMETER, NULL);

    ecs_entity_t result = desc->entity;
    if (!result) {
        result = ecs_new(world, 0);
    }

    ecs_filter_desc_t private_desc = desc->filter;
    private_desc.entity = result;

    ecs_rule_t *rule = ecs_rule_init(world, &private_desc);
    if (!rule) {
        return 0;
    }

    const ecs_filter_t *filter = ecs_rule_get_filter(rule);
    if (!(filter->flags & EcsFilterMatchThis)) {
        ecs_err("alert filter must have at least one '$this' term");
        ecs_rule_fini(rule);
        return 0;
    }

    /* Initialize Alert component which identifiers entity as alert */
    EcsAlert *alert = ecs_get_mut(world, result, EcsAlert);
    ecs_assert(alert != NULL, ECS_INTERNAL_ERROR, NULL);
    alert->message = ecs_os_strdup(desc->message);
    ecs_modified(world, result, EcsAlert);

    /* Register alert as metric */
    ecs_add(world, result, EcsMetric);
    ecs_add_pair(world, result, EcsMetric, EcsCounter);

    if (desc->brief) {
#ifdef FLECS_DOC
        ecs_doc_set_brief(world, result, desc->brief);
#else
        ecs_err("cannot set brief for alert, requires FLECS_DOC addon");
        goto error;
#endif
    }

    return result;
error:
    return 0;
}

int32_t ecs_get_alert_count(
    const ecs_world_t *world,
    ecs_entity_t entity,
    ecs_entity_t alert)
{
    ecs_poly_assert(world, ecs_world_t);
    ecs_check(entity != 0, ECS_INVALID_PARAMETER, NULL);
    ecs_check(!alert || ecs_has(world, alert, EcsAlert), 
        ECS_INVALID_PARAMETER, NULL);

    const EcsAlertsActive *active = ecs_get(world, entity, EcsAlertsActive);
    if (!active) {
        return 0;
    }

    if (alert) {
        return ecs_map_get(&active->alerts, alert) != NULL;
    }

    return ecs_map_count(&active->alerts);
error:
    return 0;
}

ecs_entity_t ecs_get_alert(
    const ecs_world_t *world,
    ecs_entity_t entity,
    ecs_entity_t alert)
{
    ecs_poly_assert(world, ecs_world_t);
    ecs_check(entity != 0, ECS_INVALID_PARAMETER, NULL);
    ecs_check(alert != 0, ECS_INVALID_PARAMETER, NULL);

    const EcsAlertsActive *active = ecs_get(world, entity, EcsAlertsActive);
    if (!active) {
        return 0;
    }

    ecs_entity_t *ptr = ecs_map_get(&active->alerts, alert);
    if (ptr) {
        return ptr[0];
    }

error:
    return 0;
}

void FlecsAlertsImport(ecs_world_t *world) {
    ECS_MODULE_DEFINE(world, FlecsAlerts);

    ECS_IMPORT(world, FlecsPipeline);
    ECS_IMPORT(world, FlecsTimer);
    ECS_IMPORT(world, FlecsMetrics);
#ifdef FLECS_DOC
    ECS_IMPORT(world, FlecsDoc);
#endif

    ecs_set_name_prefix(world, "Ecs");
    ECS_COMPONENT_DEFINE(world, EcsAlert);

    ecs_set_name_prefix(world, "EcsAlert");
    ECS_COMPONENT_DEFINE(world, EcsAlertInstance);
    ECS_COMPONENT_DEFINE(world, EcsAlertsActive);

    ecs_add_id(world, ecs_id(EcsAlertsActive), EcsPrivate);

    ecs_struct(world, {
        .entity = ecs_id(EcsAlertInstance),
        .members = {
            { .name = "message", .type = ecs_id(ecs_string_t) }
        }
    });

    ecs_set_hooks(world, EcsAlert, {
        .ctor = ecs_ctor(EcsAlert),
        .dtor = ecs_dtor(EcsAlert),
        .move = ecs_move(EcsAlert)
    });

    ecs_set_hooks(world, EcsAlertsActive, {
        .ctor = ecs_ctor(EcsAlertsActive),
        .dtor = ecs_dtor(EcsAlertsActive),
        .move = ecs_move(EcsAlertsActive)
    });

    ECS_SYSTEM(world, MonitorAlerts, EcsPreStore, Alert, (Poly, Query));
    ECS_SYSTEM(world, MonitorAlertInstances, EcsOnStore, Instance, 
        flecs.metrics.Source, flecs.metrics.Value);

    ecs_system(world, {
        .entity = ecs_id(MonitorAlerts),
        .no_readonly = true,
        .interval = 1.0
    });

    ecs_system(world, {
        .entity = ecs_id(MonitorAlertInstances),
        .interval = 1.0
    });
}

#endif

/**
 * @file addons/os_api_impl/os_api_impl.c
 * @brief Builtin implementation for OS API.
 */


#ifdef FLECS_OS_API_IMPL
#ifdef ECS_TARGET_WINDOWS
/**
 * @file addons/os_api_impl/posix_impl.inl
 * @brief Builtin Windows implementation for OS API.
 */

#ifndef WIN32_LEAN_AND_MEAN
#define WIN32_LEAN_AND_MEAN
#endif
#include <winsock2.h>
#include <windows.h>

static
ecs_os_thread_t win_thread_new(
    ecs_os_thread_callback_t callback, 
    void *arg)
{
    HANDLE *thread = ecs_os_malloc_t(HANDLE);
    *thread = CreateThread(
        NULL, 0, (LPTHREAD_START_ROUTINE)callback, arg, 0, NULL);
    return (ecs_os_thread_t)(uintptr_t)thread;
}

static
void* win_thread_join(
    ecs_os_thread_t thr)
{
    HANDLE *thread = (HANDLE*)(uintptr_t)thr;
    DWORD r = WaitForSingleObject(*thread, INFINITE);
    if (r == WAIT_FAILED) {
        ecs_err("win_thread_join: WaitForSingleObject failed");
    }
    ecs_os_free(thread);
    return NULL;
}

static
ecs_os_thread_id_t win_thread_self(void)
{
    return (ecs_os_thread_id_t)GetCurrentThreadId();
}

static
int32_t win_ainc(
    int32_t *count) 
{
    return InterlockedIncrement(count);
}

static
int32_t win_adec(
    int32_t *count) 
{
    return InterlockedDecrement(count);
}

static
int64_t win_lainc(
    int64_t *count) 
{
    return InterlockedIncrement64(count);
}

static
int64_t win_ladec(
    int64_t *count) 
{
    return InterlockedDecrement64(count);
}

static
ecs_os_mutex_t win_mutex_new(void) {
    CRITICAL_SECTION *mutex = ecs_os_malloc_t(CRITICAL_SECTION);
    InitializeCriticalSection(mutex);
    return (ecs_os_mutex_t)(uintptr_t)mutex;
}

static
void win_mutex_free(
    ecs_os_mutex_t m) 
{
    CRITICAL_SECTION *mutex = (CRITICAL_SECTION*)(intptr_t)m;
    DeleteCriticalSection(mutex);
    ecs_os_free(mutex);
}

static
void win_mutex_lock(
    ecs_os_mutex_t m) 
{
    CRITICAL_SECTION *mutex = (CRITICAL_SECTION*)(intptr_t)m;
    EnterCriticalSection(mutex);
}

static
void win_mutex_unlock(
    ecs_os_mutex_t m) 
{
    CRITICAL_SECTION *mutex = (CRITICAL_SECTION*)(intptr_t)m;
    LeaveCriticalSection(mutex);
}

static
ecs_os_cond_t win_cond_new(void) {
    CONDITION_VARIABLE *cond = ecs_os_malloc_t(CONDITION_VARIABLE);
    InitializeConditionVariable(cond);
    return (ecs_os_cond_t)(uintptr_t)cond;
}

static 
void win_cond_free(
    ecs_os_cond_t c) 
{
    (void)c;
}

static 
void win_cond_signal(
    ecs_os_cond_t c) 
{
    CONDITION_VARIABLE *cond = (CONDITION_VARIABLE*)(intptr_t)c;
    WakeConditionVariable(cond);
}

static 
void win_cond_broadcast(
    ecs_os_cond_t c) 
{
    CONDITION_VARIABLE *cond = (CONDITION_VARIABLE*)(intptr_t)c;
    WakeAllConditionVariable(cond);
}

static 
void win_cond_wait(
    ecs_os_cond_t c, 
    ecs_os_mutex_t m) 
{
    CRITICAL_SECTION *mutex = (CRITICAL_SECTION*)(intptr_t)m;
    CONDITION_VARIABLE *cond = (CONDITION_VARIABLE*)(intptr_t)c;
    SleepConditionVariableCS(cond, mutex, INFINITE);
}

static bool win_time_initialized;
static double win_time_freq;
static LARGE_INTEGER win_time_start;

static
void win_time_setup(void) {
    if ( win_time_initialized) {
        return;
    }
    
    win_time_initialized = true;

    LARGE_INTEGER freq;
    QueryPerformanceFrequency(&freq);
    QueryPerformanceCounter(&win_time_start);
    win_time_freq = (double)freq.QuadPart / 1000000000.0;
}

static
void win_sleep(
    int32_t sec, 
    int32_t nanosec) 
{
    HANDLE timer;
    LARGE_INTEGER ft;

    ft.QuadPart = -((int64_t)sec * 10000000 + (int64_t)nanosec / 100);

    timer = CreateWaitableTimer(NULL, TRUE, NULL);
    SetWaitableTimer(timer, &ft, 0, NULL, NULL, 0);
    WaitForSingleObject(timer, INFINITE);
    CloseHandle(timer);
}

static double win_time_freq;
static ULONG win_current_resolution;

static
void win_enable_high_timer_resolution(bool enable)
{
    HMODULE hntdll = GetModuleHandle((LPCTSTR)"ntdll.dll");
    if (!hntdll) {
        return;
    }

    LONG (__stdcall *pNtSetTimerResolution)(
        ULONG desired, BOOLEAN set, ULONG * current);

    pNtSetTimerResolution = (LONG(__stdcall*)(ULONG, BOOLEAN, ULONG*))
        GetProcAddress(hntdll, "NtSetTimerResolution");

    if(!pNtSetTimerResolution) {
        return;
    }

    ULONG current, resolution = 10000; /* 1 ms */

    if (!enable && win_current_resolution) {
        pNtSetTimerResolution(win_current_resolution, 0, &current);
        win_current_resolution = 0;
        return;
    } else if (!enable) {
        return;
    }

    if (resolution == win_current_resolution) {
        return;
    }

    if (win_current_resolution) {
        pNtSetTimerResolution(win_current_resolution, 0, &current);
    }

    if (pNtSetTimerResolution(resolution, 1, &current)) {
        /* Try setting a lower resolution */
        resolution *= 2;
        if(pNtSetTimerResolution(resolution, 1, &current)) return;
    }

    win_current_resolution = resolution;
}

static
uint64_t win_time_now(void) {
    uint64_t now;

    LARGE_INTEGER qpc_t;
    QueryPerformanceCounter(&qpc_t);
    now = (uint64_t)(qpc_t.QuadPart / win_time_freq);

    return now;
}

static
void win_fini(void) {
    if (ecs_os_api.flags_ & EcsOsApiHighResolutionTimer) {
        win_enable_high_timer_resolution(false);
    }
}

void ecs_set_os_api_impl(void) {
    ecs_os_set_api_defaults();

    ecs_os_api_t api = ecs_os_api;

    api.thread_new_ = win_thread_new;
    api.thread_join_ = win_thread_join;
    api.thread_self_ = win_thread_self;
    api.task_new_ = win_thread_new;
    api.task_join_ = win_thread_join;
    api.ainc_ = win_ainc;
    api.adec_ = win_adec;
    api.lainc_ = win_lainc;
    api.ladec_ = win_ladec;
    api.mutex_new_ = win_mutex_new;
    api.mutex_free_ = win_mutex_free;
    api.mutex_lock_ = win_mutex_lock;
    api.mutex_unlock_ = win_mutex_unlock;
    api.cond_new_ = win_cond_new;
    api.cond_free_ = win_cond_free;
    api.cond_signal_ = win_cond_signal;
    api.cond_broadcast_ = win_cond_broadcast;
    api.cond_wait_ = win_cond_wait;
    api.sleep_ = win_sleep;
    api.now_ = win_time_now;
    api.fini_ = win_fini;

    win_time_setup();

    if (ecs_os_api.flags_ & EcsOsApiHighResolutionTimer) {
        win_enable_high_timer_resolution(true);
    }

    ecs_os_set_api(&api);
}

#else
/**
 * @file addons/os_api_impl/posix_impl.inl
 * @brief Builtin POSIX implementation for OS API.
 */

#include "pthread.h"

#if defined(__APPLE__) && defined(__MACH__)
#include <mach/mach_time.h>
#elif defined(__EMSCRIPTEN__)
#include <emscripten.h>
#else
#include <time.h>
#endif

/* This mutex is used to emulate atomic operations when the gnu builtins are
 * not supported. This is probably not very fast but if the compiler doesn't
 * support the gnu built-ins, then speed is probably not a priority. */
#ifndef __GNUC__
static pthread_mutex_t atomic_mutex = PTHREAD_MUTEX_INITIALIZER;
#endif

static
ecs_os_thread_t posix_thread_new(
    ecs_os_thread_callback_t callback, 
    void *arg)
{
    pthread_t *thread = ecs_os_malloc(sizeof(pthread_t));

    if (pthread_create (thread, NULL, callback, arg) != 0) {
        ecs_os_abort();
    }

    return (ecs_os_thread_t)(uintptr_t)thread;
}

static
void* posix_thread_join(
    ecs_os_thread_t thread)
{
    void *arg;
    pthread_t *thr = (pthread_t*)(uintptr_t)thread;
    pthread_join(*thr, &arg);
    ecs_os_free(thr);
    return arg;
}

static
ecs_os_thread_id_t posix_thread_self(void)
{
    return (ecs_os_thread_id_t)pthread_self();
}

static
int32_t posix_ainc(
    int32_t *count)
{
    int value;
#ifdef __GNUC__
    value = __sync_add_and_fetch (count, 1);
    return value;
#else
    if (pthread_mutex_lock(&atomic_mutex)) {
	    abort();
    }
    value = (*count) += 1;
    if (pthread_mutex_unlock(&atomic_mutex)) {
	    abort();
    }
    return value;
#endif
}

static
int32_t posix_adec(
    int32_t *count) 
{
    int32_t value;
#ifdef __GNUC__
    value = __sync_sub_and_fetch (count, 1);
    return value;
#else
    if (pthread_mutex_lock(&atomic_mutex)) {
	    abort();
    }
    value = (*count) -= 1;
    if (pthread_mutex_unlock(&atomic_mutex)) {
	    abort();
    }
    return value;
#endif
}

static
int64_t posix_lainc(
    int64_t *count)
{
    int64_t value;
#ifdef __GNUC__
    value = __sync_add_and_fetch (count, 1);
    return value;
#else
    if (pthread_mutex_lock(&atomic_mutex)) {
	    abort();
    }
    value = (*count) += 1;
    if (pthread_mutex_unlock(&atomic_mutex)) {
	    abort();
    }
    return value;
#endif
}

static
int64_t posix_ladec(
    int64_t *count) 
{
    int64_t value;
#ifdef __GNUC__
    value = __sync_sub_and_fetch (count, 1);
    return value;
#else
    if (pthread_mutex_lock(&atomic_mutex)) {
	    abort();
    }
    value = (*count) -= 1;
    if (pthread_mutex_unlock(&atomic_mutex)) {
	    abort();
    }
    return value;
#endif
}

static
ecs_os_mutex_t posix_mutex_new(void) {
    pthread_mutex_t *mutex = ecs_os_malloc(sizeof(pthread_mutex_t));
    if (pthread_mutex_init(mutex, NULL)) {
        abort();
    }
    return (ecs_os_mutex_t)(uintptr_t)mutex;
}

static
void posix_mutex_free(
    ecs_os_mutex_t m) 
{
    pthread_mutex_t *mutex = (pthread_mutex_t*)(intptr_t)m;
    pthread_mutex_destroy(mutex);
    ecs_os_free(mutex);
}

static
void posix_mutex_lock(
    ecs_os_mutex_t m) 
{
    pthread_mutex_t *mutex = (pthread_mutex_t*)(intptr_t)m;
    if (pthread_mutex_lock(mutex)) {
        abort();
    }
}

static
void posix_mutex_unlock(
    ecs_os_mutex_t m) 
{
    pthread_mutex_t *mutex = (pthread_mutex_t*)(intptr_t)m;
    if (pthread_mutex_unlock(mutex)) {
        abort();
    }
}

static
ecs_os_cond_t posix_cond_new(void) {
    pthread_cond_t *cond = ecs_os_malloc(sizeof(pthread_cond_t));
    if (pthread_cond_init(cond, NULL)) {
        abort();
    }
    return (ecs_os_cond_t)(uintptr_t)cond;
}

static 
void posix_cond_free(
    ecs_os_cond_t c) 
{
    pthread_cond_t *cond = (pthread_cond_t*)(intptr_t)c;
    if (pthread_cond_destroy(cond)) {
        abort();
    }
    ecs_os_free(cond);
}

static 
void posix_cond_signal(
    ecs_os_cond_t c) 
{
    pthread_cond_t *cond = (pthread_cond_t*)(intptr_t)c;
    if (pthread_cond_signal(cond)) {
        abort();
    }
}

static 
void posix_cond_broadcast(
    ecs_os_cond_t c) 
{
    pthread_cond_t *cond = (pthread_cond_t*)(intptr_t)c;
    if (pthread_cond_broadcast(cond)) {
        abort();
    }
}

static 
void posix_cond_wait(
    ecs_os_cond_t c, 
    ecs_os_mutex_t m) 
{
    pthread_cond_t *cond = (pthread_cond_t*)(intptr_t)c;
    pthread_mutex_t *mutex = (pthread_mutex_t*)(intptr_t)m;
    if (pthread_cond_wait(cond, mutex)) {
        abort();
    }
}

static bool posix_time_initialized;

#if defined(__APPLE__) && defined(__MACH__)
static mach_timebase_info_data_t posix_osx_timebase;
static uint64_t posix_time_start;
#else
static uint64_t posix_time_start;
#endif

static
void posix_time_setup(void) {
    if (posix_time_initialized) {
        return;
    }
    
    posix_time_initialized = true;

    #if defined(__APPLE__) && defined(__MACH__)
        mach_timebase_info(&posix_osx_timebase);
        posix_time_start = mach_absolute_time();
    #else
        struct timespec ts;
        clock_gettime(CLOCK_MONOTONIC, &ts);
        posix_time_start = (uint64_t)ts.tv_sec*1000000000 + (uint64_t)ts.tv_nsec; 
    #endif
}

static
void posix_sleep(
    int32_t sec, 
    int32_t nanosec) 
{
    struct timespec sleepTime;
    ecs_assert(sec >= 0, ECS_INTERNAL_ERROR, NULL);
    ecs_assert(nanosec >= 0, ECS_INTERNAL_ERROR, NULL);

    sleepTime.tv_sec = sec;
    sleepTime.tv_nsec = nanosec;
    if (nanosleep(&sleepTime, NULL)) {
        ecs_err("nanosleep failed");
    }
}

/* prevent 64-bit overflow when computing relative timestamp
    see https://gist.github.com/jspohr/3dc4f00033d79ec5bdaf67bc46c813e3
*/
#if defined(ECS_TARGET_DARWIN)
static
int64_t posix_int64_muldiv(int64_t value, int64_t numer, int64_t denom) {
    int64_t q = value / denom;
    int64_t r = value % denom;
    return q * numer + r * numer / denom;
}
#endif

static
uint64_t posix_time_now(void) {
    ecs_assert(posix_time_initialized != 0, ECS_INTERNAL_ERROR, NULL);

    uint64_t now;

    #if defined(ECS_TARGET_DARWIN)
        now = (uint64_t) posix_int64_muldiv(
            (int64_t)mach_absolute_time(), 
            (int64_t)posix_osx_timebase.numer, 
            (int64_t)posix_osx_timebase.denom);
    #elif defined(__EMSCRIPTEN__)
        now = (long long)(emscripten_get_now() * 1000.0 * 1000);
    #else
        struct timespec ts;
        clock_gettime(CLOCK_MONOTONIC, &ts);
        now = ((uint64_t)ts.tv_sec * 1000 * 1000 * 1000 + (uint64_t)ts.tv_nsec);
    #endif

    return now;
}

void ecs_set_os_api_impl(void) {
    ecs_os_set_api_defaults();

    ecs_os_api_t api = ecs_os_api;

    api.thread_new_ = posix_thread_new;
    api.thread_join_ = posix_thread_join;
    api.thread_self_ = posix_thread_self;
    api.task_new_ = posix_thread_new;
    api.task_join_ = posix_thread_join;
    api.ainc_ = posix_ainc;
    api.adec_ = posix_adec;
    api.lainc_ = posix_lainc;
    api.ladec_ = posix_ladec;
    api.mutex_new_ = posix_mutex_new;
    api.mutex_free_ = posix_mutex_free;
    api.mutex_lock_ = posix_mutex_lock;
    api.mutex_unlock_ = posix_mutex_unlock;
    api.cond_new_ = posix_cond_new;
    api.cond_free_ = posix_cond_free;
    api.cond_signal_ = posix_cond_signal;
    api.cond_broadcast_ = posix_cond_broadcast;
    api.cond_wait_ = posix_cond_wait;
    api.sleep_ = posix_sleep;
    api.now_ = posix_time_now;

    posix_time_setup();

    ecs_os_set_api(&api);
}

#endif
#endif

/**
 * @file addons/plecs.c
 * @brief Plecs addon.
 */


#ifdef FLECS_PLECS

ECS_COMPONENT_DECLARE(EcsScript);

#include <ctype.h>

#define TOK_NEWLINE '\n'
#define TOK_USING "using"
#define TOK_MODULE "module"
#define TOK_WITH "with"
#define TOK_CONST "const"
#define TOK_PROP "prop"
#define TOK_ASSEMBLY "assembly"

#define STACK_MAX_SIZE (64)

typedef struct {
    ecs_value_t value;
    bool owned;
} plecs_with_value_t;

typedef struct {
    const char *name;
    const char *code;

    ecs_entity_t last_predicate;
    ecs_entity_t last_subject;
    ecs_entity_t last_object;

    ecs_id_t last_assign_id;
    ecs_entity_t assign_to;

    ecs_entity_t scope[STACK_MAX_SIZE];
    ecs_entity_t default_scope_type[STACK_MAX_SIZE];
    ecs_entity_t with[STACK_MAX_SIZE];
    ecs_entity_t using[STACK_MAX_SIZE];
    int32_t with_frames[STACK_MAX_SIZE];
    plecs_with_value_t with_value_frames[STACK_MAX_SIZE];
    int32_t using_frames[STACK_MAX_SIZE];
    int32_t sp;
    int32_t with_frame;
    int32_t using_frame;
    ecs_entity_t global_with;
    ecs_entity_t assembly;
    const char *assembly_start, *assembly_stop;

    char *annot[STACK_MAX_SIZE];
    int32_t annot_count;

    ecs_vars_t vars;
    char var_name[256];
    ecs_entity_t var_type;

    bool with_stmt;
    bool scope_assign_stmt;
    bool assign_stmt;
    bool assembly_stmt;
    bool assembly_instance;
    bool isa_stmt;
    bool decl_stmt;
    bool decl_type;
    bool var_stmt;
    bool var_is_prop;
    bool is_module;

    int32_t errors;
} plecs_state_t;

static
int flecs_plecs_parse(
    ecs_world_t *world,
    const char *name,
    const char *expr,
    ecs_vars_t *vars,
    ecs_entity_t script,
    ecs_entity_t instance);

static void flecs_dtor_script(EcsScript *ptr) {
    ecs_os_free(ptr->script);
    ecs_vec_fini_t(NULL, &ptr->using_, ecs_entity_t);

    int i, count = ptr->prop_defaults.count;
    ecs_value_t *values = ptr->prop_defaults.array;
    for (i = 0; i < count; i ++) {
        ecs_value_free(ptr->world, values[i].type, values[i].ptr);
    }

    ecs_vec_fini_t(NULL, &ptr->prop_defaults, ecs_value_t);
}

static
ECS_MOVE(EcsScript, dst, src, {
    flecs_dtor_script(dst);
    dst->using_ = src->using_;
    dst->prop_defaults = src->prop_defaults;
    dst->script = src->script;
    dst->world = src->world;
    ecs_os_zeromem(&src->using_);
    ecs_os_zeromem(&src->prop_defaults);
    src->script = NULL;
    src->world = NULL;
})

static
ECS_DTOR(EcsScript, ptr, {
    flecs_dtor_script(ptr);
})

/* Assembly ctor to initialize with default property values */
static
void flecs_assembly_ctor(
    void *ptr,
    int32_t count,
    const ecs_type_info_t *ti)
{
    ecs_world_t *world = ti->hooks.ctx;
    ecs_entity_t assembly = ti->component;
    const EcsStruct *st = ecs_get(world, assembly, EcsStruct);

    if (!st) {
        ecs_err("assembly '%s' is not a struct, cannot construct", ti->name);
        return;
    }

    const EcsScript *script = ecs_get(world, assembly, EcsScript);
    if (!script) {
        ecs_err("assembly '%s' is not a script, cannot construct", ti->name);
        return;
    }

    if (st->members.count != script->prop_defaults.count) {
        ecs_err("number of props (%d) of assembly '%s' does not match members"
            " (%d), cannot construct", script->prop_defaults.count, 
                ti->name, st->members.count);
        return;
    }

    const ecs_member_t *members = st->members.array;
    int32_t i, m, member_count = st->members.count;
    ecs_value_t *values = script->prop_defaults.array;
    for (m = 0; m < member_count; m ++) {
        const ecs_member_t *member = &members[m];
        ecs_value_t *value = &values[m];
        const ecs_type_info_t *mti = ecs_get_type_info(world, member->type);
        if (!mti) {
            ecs_err("failed to get type info for prop '%s' of assembly '%s'",
                member->name, ti->name);
            return;
        }

        for (i = 0; i < count; i ++) {
            void *el = ECS_ELEM(ptr, ti->size, i);
            ecs_value_copy_w_type_info(world, mti, 
                ECS_OFFSET(el, member->offset), value->ptr);
        }
    }
}

/* Assembly on_set handler to update contents for new property values */
static
void flecs_assembly_on_set(
    ecs_iter_t *it)
{
    if (it->table->flags & EcsTableIsPrefab) {
        /* Don't instantiate assemblies for prefabs */
        return;
    }

    ecs_world_t *world = it->world;
    ecs_entity_t assembly = ecs_field_id(it, 1);
    const char *name = ecs_get_name(world, assembly);
    ecs_record_t *r = ecs_record_find(world, assembly);

    const EcsComponent *ct = ecs_record_get(world, r, EcsComponent);
    ecs_get(world, assembly, EcsComponent);
    if (!ct) {
        ecs_err("assembly '%s' is not a component", name);
        return;
    }

    const EcsStruct *st = ecs_record_get(world, r, EcsStruct);
    if (!st) {
        ecs_err("assembly '%s' is not a struct", name);
        return;
    }

    const EcsScript *script = ecs_record_get(world, r, EcsScript);
    if (!script) {
        ecs_err("assembly '%s' is missing a script", name);
        return;
    }

    void *data = ecs_field_w_size(it, flecs_ito(size_t, ct->size), 1);

    int32_t i, m;
    for (i = 0; i < it->count; i ++) {
        /* Create variables to hold assembly properties */
        ecs_vars_t vars = {0};
        ecs_vars_init(world, &vars);

        /* Populate properties from assembly members */
        const ecs_member_t *members = st->members.array;
        for (m = 0; m < st->members.count; m ++) {
            const ecs_member_t *member = &members[m];

            ecs_value_t v = {0}; /* Prevent allocating value */
            ecs_expr_var_t *var = ecs_vars_declare_w_value(
                &vars, member->name, &v);
            if (var == NULL) {
                ecs_err("could not create prop '%s' for assembly '%s'", 
                    member->name, name);
                break;
            }

            /* Assign assembly property from assembly instance */
            var->value.type = member->type;
            var->value.ptr = ECS_OFFSET(data, member->offset);
            var->owned = false;
        }

        /* Update script with new code/properties */
        ecs_entity_t instance = it->entities[i];
        ecs_script_update(world, assembly, instance, script->script, &vars);
        ecs_vars_fini(&vars);

        if (ecs_record_has_id(world, r, EcsFlatten)) {
            ecs_flatten(it->real_world, ecs_childof(instance), NULL);
        }

        data = ECS_OFFSET(data, ct->size);
    }
}

/* Delete contents of assembly instance */
static
void flecs_assembly_on_remove(
    ecs_iter_t *it)
{
    int32_t i;
    for (i = 0; i < it->count; i ++) {
        ecs_entity_t instance = it->entities[i];
        ecs_script_clear(it->world, 0, instance);
    }
}

/* Set default property values on assembly Script component */
static
int flecs_assembly_init_defaults(
    ecs_world_t *world,
    const char *name,
    const char *expr,
    const char *ptr,
    ecs_entity_t assembly,
    EcsScript *script,
    plecs_state_t *state)
{
    const EcsStruct *st = ecs_get(world, assembly, EcsStruct);
    int32_t i, count = st->members.count;
    const ecs_member_t *members = st->members.array;

    ecs_vec_init_t(NULL, &script->prop_defaults, ecs_value_t, count);

    for (i = 0; i < count; i ++) {
        const ecs_member_t *member = &members[i];
        ecs_expr_var_t *var = ecs_vars_lookup(&state->vars, member->name);
        if (!var) {
            char *assembly_name = ecs_get_fullpath(world, assembly);
            ecs_parser_error(name, expr, ptr - expr, 
                "missing property '%s' for assembly '%s'", 
                    member->name, assembly_name);
            ecs_os_free(assembly_name);
            return -1;
        }

        if (member->type != var->value.type) {
            char *assembly_name = ecs_get_fullpath(world, assembly);
            ecs_parser_error(name, expr, ptr - expr, 
                "property '%s' for assembly '%s' has mismatching type", 
                    member->name, assembly_name);
            ecs_os_free(assembly_name);
            return -1;
        }

        ecs_value_t *pv = ecs_vec_append_t(NULL, 
            &script->prop_defaults, ecs_value_t);
        pv->type = member->type;
        pv->ptr = var->value.ptr;
        var->owned = false; /* Transfer ownership */
    }

    return 0;
}

/* Create new assembly */
static
int flecs_assembly_create(
    ecs_world_t *world,
    const char *name,
    const char *expr,
    const char *ptr,
    ecs_entity_t assembly,
    char *script_code,
    plecs_state_t *state)
{
    const EcsStruct *st = ecs_get(world, assembly, EcsStruct);
    if (!st || !st->members.count) {
        char *assembly_name = ecs_get_fullpath(world, assembly);
        ecs_parser_error(name, expr, ptr - expr, 
            "assembly '%s' has no properties", assembly_name);
        ecs_os_free(assembly_name);
        ecs_os_free(script_code);
        return -1;
    }

    ecs_add_id(world, assembly, EcsAlwaysOverride);

    EcsScript *script = ecs_get_mut(world, assembly, EcsScript);
    flecs_dtor_script(script);
    script->world = world;
    script->script = script_code;
    ecs_vec_reset_t(NULL, &script->using_, ecs_entity_t);

    ecs_entity_t scope = ecs_get_scope(world);
    if (scope && (scope = ecs_get_target(world, scope, EcsChildOf, 0))) {
        ecs_vec_append_t(NULL, &script->using_, ecs_entity_t)[0] = scope;
    }

    int i, count = state->using_frame;
    for (i = 0; i < count; i ++) {
        ecs_vec_append_t(NULL, &script->using_, ecs_entity_t)[0] = 
            state->using[i];
    }

    if (flecs_assembly_init_defaults(
        world, name, expr, ptr, assembly, script, state)) 
    {
        return -1;    
    }

    ecs_modified(world, assembly, EcsScript);

    ecs_set_hooks_id(world, assembly, &(ecs_type_hooks_t) {
        .ctor = flecs_assembly_ctor,
        .on_set = flecs_assembly_on_set,
        .on_remove = flecs_assembly_on_remove,
        .ctx = world
    });

    return 0;
}

/* Parser */

static
bool plecs_is_newline_comment(
    const char *ptr)
{
    if (ptr[0] == '/' && ptr[1] == '/') {
        return true;
    }
    return false;
}

static
const char* plecs_parse_fluff(
    const char *ptr)
{
    do {
        /* Skip whitespaces before checking for a comment */
        ptr = ecs_parse_ws(ptr);

        /* Newline comment, skip until newline character */
        if (plecs_is_newline_comment(ptr)) {
            ptr += 2;

            while (ptr[0] && ptr[0] != TOK_NEWLINE) {
                ptr ++;
            }
        }

        /* If a newline character is found, skip it */
        if (ptr[0] == TOK_NEWLINE) {
            ptr ++;
        }

    } while (isspace(ptr[0]) || plecs_is_newline_comment(ptr));

    return ptr;
}

static
ecs_entity_t plecs_lookup(
    const ecs_world_t *world,
    const char *path,
    plecs_state_t *state,
    ecs_entity_t rel,
    bool is_subject)
{
    ecs_entity_t e = 0;

    if (!is_subject) {
        ecs_entity_t oneof = 0;
        if (rel) {
            if (ecs_has_id(world, rel, EcsOneOf)) {
                oneof = rel;
            } else {
                oneof = ecs_get_target(world, rel, EcsOneOf, 0);
            }
            if (oneof) {
                return ecs_lookup_path_w_sep(
                    world, oneof, path, NULL, NULL, false);
            }
        }
        int using_scope = state->using_frame - 1;
        for (; using_scope >= 0; using_scope--) {
            e = ecs_lookup_path_w_sep(
                world, state->using[using_scope], path, NULL, NULL, false);
            if (e) {
                break;
            }
        }
    }

    if (!e) {
        e = ecs_lookup_path_w_sep(world, 0, path, NULL, NULL, !is_subject);
    }

    return e;
}

/* Lookup action used for deserializing entity refs in component values */
static
ecs_entity_t plecs_lookup_action(
    const ecs_world_t *world,
    const char *path,
    void *ctx)
{
    return plecs_lookup(world, path, ctx, 0, false);
}

static
ecs_entity_t plecs_ensure_entity(
    ecs_world_t *world,
    plecs_state_t *state,
    const char *path,
    ecs_entity_t rel,
    bool is_subject)
{
    if (!path) {
        return 0;
    }

    ecs_entity_t e = 0;
    bool is_anonymous = !ecs_os_strcmp(path, "_");
    bool is_new = false;
    if (is_anonymous) {
        path = NULL;
        e = ecs_new_id(world);
        is_new = true;
    }

    if (!e) {
        e = plecs_lookup(world, path, state, rel, is_subject);
    }

    if (!e) {
        is_new = true;
        if (rel && flecs_get_oneof(world, rel)) {
            /* If relationship has oneof and entity was not found, don't proceed
             * with creating an entity as this can cause asserts later on */
            char *relstr = ecs_get_fullpath(world, rel);
            ecs_parser_error(state->name, 0, 0, 
                "invalid identifier '%s' for relationship '%s'", path, relstr);
            ecs_os_free(relstr);
            return 0;
        }

        ecs_entity_t prev_scope = 0;
        ecs_entity_t prev_with = 0;
        if (!is_subject) {
            /* Don't apply scope/with for non-subject entities */
            prev_scope = ecs_set_scope(world, 0);
            prev_with = ecs_set_with(world, 0);
        }

        e = ecs_add_path(world, e, 0, path);
        ecs_assert(e != 0, ECS_INTERNAL_ERROR, NULL);

        if (prev_scope) {
            ecs_set_scope(world, prev_scope);
        }
        if (prev_with) {
            ecs_set_with(world, prev_with);
        }
    } else {
        /* If entity exists, make sure it gets the right scope and with */
        if (is_subject) {
            ecs_entity_t scope = ecs_get_scope(world);
            if (scope) {
                ecs_add_pair(world, e, EcsChildOf, scope);
            }

            ecs_entity_t with = ecs_get_with(world);
            if (with) {
                ecs_add_id(world, e, with);
            }
        }
    }

    if (is_new) {
        if (state->assembly && !state->assembly_instance) {
            ecs_add_id(world, e, EcsPrefab);
        }

        if (state->global_with) {
            ecs_add_id(world, e, state->global_with);
        }
    }

    return e;
}

static
bool plecs_pred_is_subj(
    ecs_term_t *term,
    plecs_state_t *state)
{
    if (term->src.name != NULL) {
        return false;
    }
    if (term->second.name != NULL) {
        return false;
    }
    if (ecs_term_match_0(term)) {
        return false;
    }
    if (state->with_stmt) {
        return false;
    }
    if (state->assign_stmt) {
        return false;
    }
    if (state->isa_stmt) {
        return false;
    }
    if (state->decl_type) {
        return false;
    }

    return true;
}

/* Set masks aren't useful in plecs, so translate them back to entity names */
static
const char* plecs_set_mask_to_name(
    ecs_flags32_t flags) 
{
    flags &= EcsTraverseFlags;
    if (flags == EcsSelf) {
        return "self";
    } else if (flags == EcsUp) {
        return "up";
    } else if (flags == EcsDown) {
        return "down";
    } else if (flags == EcsCascade || flags == (EcsUp|EcsCascade)) {
        return "cascade";
    } else if (flags == EcsParent) {
        return "parent";
    }
    return NULL;
}

static
char* plecs_trim_annot(
    char *annot)
{
    annot = (char*)ecs_parse_ws(annot);
    int32_t len = ecs_os_strlen(annot) - 1;
    while (isspace(annot[len]) && (len > 0)) {
        annot[len] = '\0';
        len --;
    }
    return annot;
}

static
void plecs_apply_annotations(
    ecs_world_t *world,
    ecs_entity_t subj,
    plecs_state_t *state)
{
    (void)world;
    (void)subj;
    (void)state;
#ifdef FLECS_DOC
    int32_t i = 0, count = state->annot_count;
    for (i = 0; i < count; i ++) {
        char *annot = state->annot[i];
        if (!ecs_os_strncmp(annot, "@brief ", 7)) {
            annot = plecs_trim_annot(annot + 7);
            ecs_doc_set_brief(world, subj, annot);
        } else if (!ecs_os_strncmp(annot, "@link ", 6)) {
            annot = plecs_trim_annot(annot + 6);
            ecs_doc_set_link(world, subj, annot);
        } else if (!ecs_os_strncmp(annot, "@name ", 6)) {
            annot = plecs_trim_annot(annot + 6);
            ecs_doc_set_name(world, subj, annot);
        } else if (!ecs_os_strncmp(annot, "@color ", 7)) {
            annot = plecs_trim_annot(annot + 7);
            ecs_doc_set_color(world, subj, annot);
        }
    }
#else
    ecs_warn("cannot apply annotations, doc addon is missing");
#endif
}

static
int plecs_create_term(
    ecs_world_t *world, 
    ecs_term_t *term,
    const char *name,
    const char *expr,
    int64_t column,
    plecs_state_t *state)
{
    state->last_subject = 0;
    state->last_predicate = 0;
    state->last_object = 0;
    state->last_assign_id = 0;

    const char *pred_name = term->first.name;
    const char *subj_name = term->src.name;
    const char *obj_name = term->second.name;

    if (!subj_name) {
        subj_name = plecs_set_mask_to_name(term->src.flags);
    }
    if (!obj_name) {
        obj_name = plecs_set_mask_to_name(term->second.flags);
    }

    if (!ecs_term_id_is_set(&term->first)) {
        ecs_parser_error(name, expr, column, "missing predicate in expression");
        return -1;
    }

    if (state->assign_stmt && !ecs_term_match_this(term)) {
        ecs_parser_error(name, expr, column, 
            "invalid statement in assign statement");
        return -1;
    }

    bool pred_as_subj = plecs_pred_is_subj(term, state);
    ecs_entity_t pred = plecs_ensure_entity(world, state, pred_name, 0, pred_as_subj); 
    ecs_entity_t subj = plecs_ensure_entity(world, state, subj_name, pred, true);
    ecs_entity_t obj = 0;

    if (ecs_term_id_is_set(&term->second)) {
        obj = plecs_ensure_entity(world, state, obj_name, pred,
            !state->assign_stmt && !state->with_stmt);
        if (!obj) {
            return -1;
        }
    }

    if (state->assign_stmt || state->isa_stmt) {
        subj = state->assign_to;
    }

    if (state->isa_stmt && obj) {
        ecs_parser_error(name, expr, column, 
            "invalid object in inheritance statement");
        return -1;
    }

    if (state->isa_stmt) {
        pred = ecs_pair(EcsIsA, pred);
    }

    if (subj == EcsVariable) {
        subj = pred;
    }

    if (subj) {
        if (!obj) {
            ecs_add_id(world, subj, pred);
            state->last_assign_id = pred;
        } else {
            ecs_add_pair(world, subj, pred, obj);
            state->last_object = obj;
            state->last_assign_id = ecs_pair(pred, obj);
        }
        state->last_predicate = pred;
        state->last_subject = subj;

        pred_as_subj = false;
    } else {
        if (!obj) {
            /* If no subject or object were provided, use predicate as subj 
             * unless the expression explictly excluded the subject */
            if (pred_as_subj) {
                state->last_subject = pred;
                subj = pred;
            } else {
                state->last_predicate = pred;
                pred_as_subj = false;
            }
        } else {
            state->last_predicate = pred;
            state->last_object = obj;
            pred_as_subj = false;
        }
    }

    /* If this is a with clause (the list of entities between 'with' and scope
     * open), add subject to the array of with frames */
    if (state->with_stmt) {
        ecs_assert(pred != 0, ECS_INTERNAL_ERROR, NULL);
        ecs_id_t id;

        if (obj) {
            id = ecs_pair(pred, obj);
        } else {
            id = pred;
        }

        state->with[state->with_frame ++] = id;

    } else {
        if (subj) {
            int32_t i, frame_count = state->with_frames[state->sp];
            for (i = 0; i < frame_count; i ++) {
                ecs_id_t id = state->with[i];
                plecs_with_value_t *v = &state->with_value_frames[i];
                if (v->value.type) {
                    void *ptr = ecs_get_mut_id(world, subj, id);
                    ecs_value_copy(world, v->value.type, ptr, v->value.ptr);
                    ecs_modified_id(world, subj, id);
                } else {
                    ecs_add_id(world, subj, id);
                }
            }
        }
    }

    /* If an id was provided by itself, add default scope type to it */
    ecs_entity_t default_scope_type = state->default_scope_type[state->sp];
    if (pred_as_subj && default_scope_type) {
        ecs_add_id(world, subj, default_scope_type);
    }

    /* If annotations preceded the statement, append */
    if (!state->decl_type && state->annot_count) {
        if (!subj) {
            ecs_parser_error(name, expr, column, 
                "missing subject for annotations");
            return -1;
        }

        plecs_apply_annotations(world, subj, state);
    }

    return 0;
}

static
const char* plecs_parse_inherit_stmt(
    const char *name,
    const char *expr,
    const char *ptr,
    plecs_state_t *state)
{
    if (state->isa_stmt) {
        ecs_parser_error(name, expr, ptr - expr, 
            "cannot nest inheritance");
        return NULL;
    }

    if (!state->last_subject) {
        ecs_parser_error(name, expr, ptr - expr, 
            "missing entity to assign inheritance to");
        return NULL;
    }
    
    state->isa_stmt = true;
    state->assign_to = state->last_subject;

    return ptr;
}

static
const char* plecs_parse_assign_var_expr(
    ecs_world_t *world,
    const char *name,
    const char *expr,
    const char *ptr,
    plecs_state_t *state,
    ecs_expr_var_t *var)
{
    ecs_value_t value = {0};

    if (state->last_assign_id) {
        value.type = state->last_assign_id;
        value.ptr = ecs_value_new(world, state->last_assign_id);
        if (!var && state->assembly_instance) {
            var = ecs_vars_lookup(&state->vars, state->var_name);
        }
    }

    ptr = ecs_parse_expr(world, ptr, &value, 
        &(ecs_parse_expr_desc_t){
            .name = name,
            .expr = expr,
            .lookup_action = plecs_lookup_action,
            .lookup_ctx = state,
            .vars = &state->vars
        });
    if (!ptr) {
        if (state->last_assign_id) {
            ecs_value_free(world, value.type, value.ptr);
        }
        goto error;
    }

    if (var) {
        bool ignore = state->var_is_prop && state->assembly_instance;
        if (!ignore) {
            if (var->value.ptr) {
                ecs_value_free(world, var->value.type, var->value.ptr);
                var->value.ptr = value.ptr;
                var->value.type = value.type;
            }
        } else {
            ecs_value_free(world, value.type, value.ptr);
        }
    } else {
        var = ecs_vars_declare_w_value(
            &state->vars, state->var_name, &value);
        if (!var) {
            goto error;
        }
    }

    state->var_is_prop = false;
    return ptr;
error:
    return NULL;
}

static
const char* plecs_parse_assign_expr(
    ecs_world_t *world,
    const char *name,
    const char *expr,
    const char *ptr,
    plecs_state_t *state,
    ecs_expr_var_t *var) 
{
    (void)world;
    
    if (state->var_stmt) {
        return plecs_parse_assign_var_expr(world, name, expr, ptr, state, var);
    }

    if (!state->assign_stmt) {
        ecs_parser_error(name, expr, ptr - expr,
            "unexpected value outside of assignment statement");
        return NULL;
    }

    ecs_id_t assign_id = state->last_assign_id;
    if (!assign_id) {
        ecs_parser_error(name, expr, ptr - expr,
            "missing type for assignment statement");
        return NULL;
    }

    ecs_entity_t assign_to = state->assign_to;
    if (!assign_to) {
        assign_to = state->last_subject;
    }

    if (!assign_to) {
        ecs_parser_error(name, expr, ptr - expr, 
            "missing entity to assign to");
        return NULL;
    }

    ecs_entity_t type = ecs_get_typeid(world, assign_id);
    if (!type) {
        char *id_str = ecs_id_str(world, assign_id);
        ecs_parser_error(name, expr, ptr - expr, 
            "invalid assignment, '%s' is not a type", id_str);
        ecs_os_free(id_str);
        return NULL;
    }

    if (assign_to == EcsVariable) {
        assign_to = type;
    }

    void *value_ptr = ecs_get_mut_id(world, assign_to, assign_id);

    ptr = ecs_parse_expr(world, ptr, &(ecs_value_t){type, value_ptr}, 
        &(ecs_parse_expr_desc_t){
            .name = name,
            .expr = expr,
            .lookup_action = plecs_lookup_action,
            .lookup_ctx = state,
            .vars = &state->vars
        });
    if (!ptr) {
        return NULL;
    }

    ecs_modified_id(world, assign_to, assign_id);

    return ptr;
}

static
const char* plecs_parse_assign_stmt(
    ecs_world_t *world,
    const char *name,
    const char *expr,
    const char *ptr,
    plecs_state_t *state) 
{
    (void)world;

    state->isa_stmt = false;

    /* Component scope (add components to entity) */
    if (!state->assign_to) {
        if (!state->last_subject) {
            ecs_parser_error(name, expr, ptr - expr, 
                "missing entity to assign to");
            return NULL;
        }
        state->assign_to = state->last_subject;
    }

    if (state->assign_stmt) {
        ecs_parser_error(name, expr, ptr - expr, 
            "invalid assign statement in assign statement");
        return NULL;
    }

    state->assign_stmt = true;
    
    /* Assignment without a preceding component */
    if (ptr[0] == '{') {
        ecs_entity_t type = 0;

        /* If we're in a scope & last_subject is a type, assign to scope */
        if (ecs_get_scope(world) != 0) {
            type = ecs_get_typeid(world, state->last_subject);
            if (type != 0) {
                type = state->last_subject;
            }
        }

        /* If type hasn't been set yet, check if scope has default type */
        if (!type && !state->scope_assign_stmt) {
            type = state->default_scope_type[state->sp];
        }

        /* If no type has been found still, check if last with id is a type */
        if (!type && !state->scope_assign_stmt) {
            int32_t with_frame_count = state->with_frames[state->sp];
            if (with_frame_count) {
                type = state->with[with_frame_count - 1];
            }
        }

        if (!type) {
            ecs_parser_error(name, expr, ptr - expr, 
                "missing type for assignment");
            return NULL;
        }

        state->last_assign_id = type;
    }

    return ptr;
}

static
const char* plecs_parse_assign_with_stmt(
    ecs_world_t *world,
    const char *name,
    const char *expr,
    const char *ptr,
    plecs_state_t *state)
{
    int32_t with_frame = state->with_frame - 1;
    if (with_frame < 0) {
        ecs_parser_error(name, expr, ptr - expr, 
            "missing type in with value");
        return NULL;
    }

    ecs_id_t id = state->with[with_frame];
    ecs_id_record_t *idr = flecs_id_record_get(world, id);
    const ecs_type_info_t *ti = idr->type_info;
    if (!ti) {
        char *typename = ecs_id_str(world, id);
        ecs_parser_error(name, expr, ptr - expr, 
            "id '%s' in with value is not a type", typename);
        ecs_os_free(typename);
        return NULL;
    }

    plecs_with_value_t *v = &state->with_value_frames[with_frame];
    v->value.type = ti->component;
    v->value.ptr = ecs_value_new(world, ti->component);
    v->owned = true;
    if (!v->value.ptr) {
        char *typename = ecs_id_str(world, id);
        ecs_parser_error(name, expr, ptr - expr, 
            "failed to create value for '%s'", typename);
        ecs_os_free(typename);
        return NULL;
    }

    ptr = ecs_parse_expr(world, ptr, &v->value,
        &(ecs_parse_expr_desc_t){
            .name = name,
            .expr = expr,
            .lookup_action = plecs_lookup_action,
            .lookup_ctx = state,
            .vars = &state->vars
        });
    if (!ptr) {
        return NULL;
    }

    return ptr;
}

static
const char* plecs_parse_assign_with_var(
    const char *name,
    const char *expr,
    const char *ptr,
    plecs_state_t *state)
{
    ecs_assert(ptr[0] == '$', ECS_INTERNAL_ERROR, NULL);
    ecs_assert(state->with_stmt, ECS_INTERNAL_ERROR, NULL);

    char var_name[ECS_MAX_TOKEN_SIZE];
    const char *tmp = ptr;
    ptr = ecs_parse_token(name, expr, ptr + 1, var_name, 0);
    if (!ptr) {
        ecs_parser_error(name, expr, tmp - expr, 
            "unresolved variable '%s'", var_name);
        return NULL;
    }

    ecs_expr_var_t *var = ecs_vars_lookup(&state->vars, var_name);
    if (!var) {
        ecs_parser_error(name, expr, ptr - expr, 
            "unresolved variable '%s'", var_name);
        return NULL;
    }

    int32_t with_frame = state->with_frame;
    state->with[with_frame] = var->value.type;
    state->with_value_frames[with_frame].value = var->value;
    state->with_value_frames[with_frame].owned = false;
    state->with_frame ++;

    return ptr;
}

static
const char* plecs_parse_var_as_component(
    ecs_world_t *world,
    const char *name,
    const char *expr,
    const char *ptr,
    plecs_state_t *state)
{
    ecs_assert(ptr[0] == '$', ECS_INTERNAL_ERROR, NULL);
    ecs_assert(!state->var_stmt, ECS_INTERNAL_ERROR, NULL);
    char var_name[ECS_MAX_TOKEN_SIZE];
    const char *tmp = ptr;
    ptr = ecs_parse_token(name, expr, ptr + 1, var_name, 0);
    if (!ptr) {
        ecs_parser_error(name, expr, tmp - expr, 
            "unresolved variable '%s'", var_name);
        return NULL;
    }

    ecs_expr_var_t *var = ecs_vars_lookup(&state->vars, var_name);
    if (!var) {
        ecs_parser_error(name, expr, ptr - expr, 
            "unresolved variable '%s'", var_name);
        return NULL;
    }

    if (!state->assign_to) {
        ecs_parser_error(name, expr, ptr - expr, 
            "missing lvalue for variable assignment '%s'", var_name);
        return NULL;
    }

    /* Use type of variable as component */
    ecs_entity_t type = var->value.type;
    ecs_entity_t assign_to = state->assign_to;
    if (!assign_to) {
        assign_to = state->last_subject;
    }

    void *dst = ecs_get_mut_id(world, assign_to, type);
    if (!dst) {
        char *type_name = ecs_get_fullpath(world, type);
        ecs_parser_error(name, expr, ptr - expr, 
            "failed to obtain component for type '%s' of variable '%s'",    
                type_name, var_name);
        ecs_os_free(type_name);
        return NULL;
    }

    if (ecs_value_copy(world, type, dst, var->value.ptr)) {
        char *type_name = ecs_get_fullpath(world, type);
        ecs_parser_error(name, expr, ptr - expr, 
            "failed to copy value for variable '%s' of type '%s'",    
                var_name, type_name);
        ecs_os_free(type_name);
        return NULL;
    }

    ecs_modified_id(world, assign_to, type);

    return ptr;
}

static
void plecs_push_using(
    ecs_entity_t scope,
    plecs_state_t *state)
{
    for (int i = 0; i < state->using_frame; i ++) {
        if (state->using[i] == scope) {
            return;
        }
    }

    state->using[state->using_frame ++] = scope;
}

static
const char* plecs_parse_using_stmt(
    ecs_world_t *world,
    const char *name,
    const char *expr,
    const char *ptr,
    plecs_state_t *state) 
{
    if (state->isa_stmt || state->assign_stmt) {
        ecs_parser_error(name, expr, ptr - expr, 
            "invalid usage of using keyword");
        return NULL;
    }

    char using_path[ECS_MAX_TOKEN_SIZE];
    const char *tmp = ptr + 1;
    ptr = ecs_parse_token(name, expr, ptr + 5, using_path, 0);
    if (!ptr) {
        ecs_parser_error(name, expr, tmp - expr, 
            "expected identifier for using statement");
        return NULL;
    }

    ecs_size_t len = ecs_os_strlen(using_path);
    if (!len) {
        ecs_parser_error(name, expr, tmp - expr, 
            "missing identifier for using statement");
        return NULL;
    }

    /* Lookahead as * is not matched by parse_token */
    if (ptr[0] == '*') {
        using_path[len] = '*';
        using_path[len + 1] = '\0';
        len ++;
        ptr ++;
    }

    ecs_entity_t scope;
    if (len > 2 && !ecs_os_strcmp(&using_path[len - 2], ".*")) {
        using_path[len - 2] = '\0';
        scope = ecs_lookup_fullpath(world, using_path);
        if (!scope) {
            ecs_parser_error(name, expr, ptr - expr,
                "unresolved identifier '%s' in using statement", using_path);
            return NULL;
        }

        /* Add each child of the scope to using stack */
        ecs_iter_t it = ecs_term_iter(world, &(ecs_term_t){ 
            .id = ecs_childof(scope) });
        while (ecs_term_next(&it)) {
            int32_t i, count = it.count;
            for (i = 0; i < count; i ++) {
                plecs_push_using(it.entities[i], state);
            }
        }
    } else {
        scope = plecs_ensure_entity(world, state, using_path, 0, false);
        if (!scope) {
            ecs_parser_error(name, expr, ptr - expr,
                "unresolved identifier '%s' in using statement", using_path);
            return NULL;
        }

        plecs_push_using(scope, state);
    }

    state->using_frames[state->sp] = state->using_frame;
    return ptr;
}

static
const char* plecs_parse_module_stmt(
    ecs_world_t *world,
    const char *name,
    const char *expr,
    const char *ptr,
    plecs_state_t *state)
{
    const char *expr_start = ecs_parse_ws_eol(expr);
    if (expr_start != ptr) {
        ecs_parser_error(name, expr, ptr - expr, 
            "module must be first statement of script");
        return NULL;
    }

    char module_path[ECS_MAX_TOKEN_SIZE];
    const char *tmp = ptr + 1;
    ptr = ecs_parse_token(name, expr, ptr + 6, module_path, 0);
    if (!ptr) {
        ecs_parser_error(name, expr, tmp - expr, 
            "expected identifier for module statement");
        return NULL;
    }

    ecs_component_desc_t desc = {0};
    desc.entity = ecs_entity(world, { .name = module_path });
    ecs_entity_t module = ecs_module_init(world, NULL, &desc);
    if (!module) {
        return NULL;
    }

    state->is_module = true;
    state->sp ++;
    state->scope[state->sp] = module;
    ecs_set_scope(world, module);
    return ptr;
}

static
const char* plecs_parse_with_stmt(
    const char *name,
    const char *expr,
    const char *ptr,
    plecs_state_t *state) 
{
    if (state->isa_stmt) {
        ecs_parser_error(name, expr, ptr - expr, 
            "invalid with after inheritance");
        return NULL;
    }

    if (state->assign_stmt) {
        ecs_parser_error(name, expr, ptr - expr, 
            "invalid with in assign_stmt");
        return NULL;
    }

    /* Add following expressions to with list */
    state->with_stmt = true;
    return ptr + 5;
}

static
const char* plecs_parse_assembly_stmt(
    const char *name,
    const char *expr,
    const char *ptr,
    plecs_state_t *state) 
{
    if (state->isa_stmt) {
        ecs_parser_error(name, expr, ptr - expr, 
            "invalid with after inheritance");
        return NULL;
    }

    if (state->assign_stmt) {
        ecs_parser_error(name, expr, ptr - expr, 
            "invalid with in assign_stmt");
        return NULL;
    }

    state->assembly_stmt = true;
    return ptr + 9;
}

static
const char* plecs_parse_var_type(
    ecs_world_t *world,
    const char *name,
    const char *expr,
    const char *ptr,
    plecs_state_t *state,
    ecs_entity_t *type_out)
{
    char prop_type_name[ECS_MAX_TOKEN_SIZE];
    const char *tmp = ptr + 1;
    ptr = ecs_parse_token(name, expr, ptr + 1, prop_type_name, 0);
    if (!ptr) {
        ecs_parser_error(name, expr, tmp - expr, 
            "expected type for prop declaration");
        return NULL;
    }

    ecs_entity_t prop_type = plecs_lookup(world, prop_type_name, state, 0, false);
    if (!prop_type) {
        ecs_parser_error(name, expr, ptr - expr, 
            "unresolved property type '%s'", prop_type_name);
        return NULL;
    }

    *type_out = prop_type;

    return ptr;
}

static
const char* plecs_parse_const_stmt(
    ecs_world_t *world,
    const char *name,
    const char *expr,
    const char *ptr,
    plecs_state_t *state)
{
    ptr = ecs_parse_token(name, expr, ptr + 5, state->var_name, 0);
    if (!ptr) {
        return NULL;
    }

    ptr = ecs_parse_ws(ptr);

    if (ptr[0] == ':') {
        ptr = plecs_parse_var_type(
            world, name, expr, ptr, state, &state->last_assign_id);
        if (!ptr) {
            return NULL;
        }

        ptr = ecs_parse_ws(ptr);
    }

    if (ptr[0] != '=') {
        ecs_parser_error(name, expr, ptr - expr, 
            "expected '=' after const declaration");
        return NULL;
    }

    state->var_stmt = true;
    return ptr + 1;
}

static
const char* plecs_parse_prop_stmt(
    ecs_world_t *world,
    const char *name,
    const char *expr,
    const char *ptr,
    plecs_state_t *state)
{
    char prop_name[ECS_MAX_TOKEN_SIZE];
    ptr = ecs_parse_token(name, expr, ptr + 5, prop_name, 0);
    if (!ptr) {
        return NULL;
    }

    ptr = ecs_parse_ws(ptr);

    if (ptr[0] != ':') {
        ecs_parser_error(name, expr, ptr - expr, 
            "expected ':' after prop declaration");
        return NULL;
    }

    ecs_entity_t prop_type;
    ptr = plecs_parse_var_type(world, name, expr, ptr, state, &prop_type);
    if (!ptr) {
        return NULL;
    }

    ecs_entity_t assembly = state->assembly;
    if (!assembly) {
        ecs_parser_error(name, expr, ptr - expr, 
            "unexpected prop '%s' outside of assembly", prop_name);
        return NULL;
    }

    if (!state->assembly_instance) {
        ecs_entity_t prop_member = ecs_entity(world, {
            .name = prop_name,
            .add = { ecs_childof(assembly) }
        });

        if (!prop_member) {
            return NULL;
        }

        ecs_set(world, prop_member, EcsMember, {
            .type = prop_type
        });
    }

    if (ptr[0] != '=') {
        ecs_parser_error(name, expr, ptr - expr, 
            "expected '=' after prop type");
        return NULL;
    }

    ecs_os_strcpy(state->var_name, prop_name);
    state->last_assign_id = prop_type;
    state->var_stmt = true;
    state->var_is_prop = true;

    return plecs_parse_fluff(ptr + 1);
}

static
const char* plecs_parse_scope_open(
    ecs_world_t *world,
    const char *name,
    const char *expr,
    const char *ptr,
    plecs_state_t *state) 
{
    state->isa_stmt = false;

    if (state->assign_stmt) {
        ecs_parser_error(name, expr, ptr - expr, 
            "invalid scope in assign_stmt");
        return NULL;
    }

    state->sp ++;

    ecs_entity_t scope = 0;
    ecs_entity_t default_scope_type = 0;

    if (!state->with_stmt) {
        if (state->last_subject) {
            scope = state->last_subject;
            ecs_set_scope(world, state->last_subject);

            /* Check if scope has a default child component */
            ecs_entity_t def_type_src = ecs_get_target_for_id(world, scope, 
                0, ecs_pair(EcsDefaultChildComponent, EcsWildcard));

            if (def_type_src) {
                default_scope_type = ecs_get_target(
                    world, def_type_src, EcsDefaultChildComponent, 0);
            }
        } else {
            if (state->last_object) {
                scope = ecs_pair(
                    state->last_predicate, state->last_object);
                ecs_set_with(world, scope);
            } else {
                if (state->last_predicate) {
                    scope = ecs_pair(EcsChildOf, state->last_predicate);
                }
                ecs_set_scope(world, state->last_predicate);
            }
        }

        state->scope[state->sp] = scope;
        state->default_scope_type[state->sp] = default_scope_type;

        if (state->assembly_stmt) {
            if (state->assembly) {
                ecs_parser_error(name, expr, ptr - expr, 
                    "invalid nested assembly");
                return NULL;
            }
            state->assembly = scope;
            state->assembly_stmt = false;
            state->assembly_start = ptr;
        }
    } else {
        state->scope[state->sp] = state->scope[state->sp - 1];
        state->default_scope_type[state->sp] = 
            state->default_scope_type[state->sp - 1];
    }

    state->using_frames[state->sp] = state->using_frame;
    state->with_frames[state->sp] = state->with_frame;
    state->with_stmt = false;

    ecs_vars_push(&state->vars);

    return ptr;
}

static
const char* plecs_parse_scope_close(
    ecs_world_t *world,
    const char *name,
    const char *expr,
    const char *ptr,
    plecs_state_t *state) 
{
    if (state->isa_stmt) {
        ecs_parser_error(name, expr, ptr - expr, 
            "invalid '}' after inheritance statement");
        return NULL;
    }

    if (state->assign_stmt) {
        ecs_parser_error(name, expr, ptr - expr, 
            "unfinished assignment before }");
        return NULL;
    }

    ecs_entity_t cur = state->scope[state->sp], assembly = state->assembly;
    if (state->sp && (cur == state->scope[state->sp - 1])) {
        /* Previous scope is also from the assembly, not found the end yet */
        cur = 0;
    }
    if (cur && cur == assembly) {
        ecs_size_t assembly_len = flecs_ito(ecs_size_t, ptr - state->assembly_start);
        if (assembly_len) {
            assembly_len --;
            char *script = ecs_os_malloc_n(char, assembly_len + 1);
            ecs_os_memcpy(script, state->assembly_start, assembly_len);
            script[assembly_len] = '\0';
            state->assembly = 0;
            state->assembly_start = NULL;

            if (flecs_assembly_create(world, name, expr, ptr, assembly, script, state)) {
                return NULL;
            }
        } else {
            ecs_parser_error(name, expr, ptr - expr, "empty assembly");
            return NULL;
        }
    }

    state->scope[state->sp] = 0;
    state->default_scope_type[state->sp] = 0;
    state->sp --;

    if (state->sp < 0) {
        ecs_parser_error(name, expr, ptr - expr, "invalid } without a {");
        return NULL;
    }

    ecs_id_t id = state->scope[state->sp];
    if (!id || ECS_HAS_ID_FLAG(id, PAIR)) {
        ecs_set_with(world, id);
    }

    if (!id || !ECS_HAS_ID_FLAG(id, PAIR)) {
        ecs_set_scope(world, id);
    }

    int32_t i, prev_with = state->with_frames[state->sp];
    for (i = prev_with; i < state->with_frame; i ++) {
        plecs_with_value_t *v = &state->with_value_frames[i];
        if (!v->owned) {
            continue;
        }
        if (v->value.type) {
            ecs_value_free(world, v->value.type, v->value.ptr);
            v->value.type = 0;
            v->value.ptr = NULL;
            v->owned = false;
        }
    }

    state->with_frame = state->with_frames[state->sp];
    state->using_frame = state->using_frames[state->sp];
    state->last_subject = 0;
    state->assign_stmt = false;

    ecs_vars_pop(&state->vars);

    return plecs_parse_fluff(ptr + 1);
}

static
const char *plecs_parse_plecs_term(
    ecs_world_t *world,
    const char *name,
    const char *expr,
    const char *ptr,
    plecs_state_t *state)
{
    ecs_term_t term = {0};
    ecs_entity_t decl_id = 0;
    if (state->decl_stmt) {
        decl_id = state->last_predicate;
    }

    ptr = ecs_parse_term(world, name, expr, ptr, &term);
    if (!ptr) {
        return NULL;
    }

    if (flecs_isident(ptr[0])) {
        state->decl_type = true;
    }

    if (!ecs_term_is_initialized(&term)) {
        ecs_parser_error(name, expr, ptr - expr, "expected identifier");
        return NULL; /* No term found */
    }

    if (plecs_create_term(world, &term, name, expr, (ptr - expr), state)) {
        ecs_term_fini(&term);
        return NULL; /* Failed to create term */
    }

    if (decl_id && state->last_subject) {
        ecs_add_id(world, state->last_subject, decl_id);
    }

    state->decl_type = false;

    ecs_term_fini(&term);

    return ptr;
}

static
const char* plecs_parse_annotation(
    const char *name,
    const char *expr,
    const char *ptr,
    plecs_state_t *state)
{
    do {
        if(state->annot_count >= STACK_MAX_SIZE) {
            ecs_parser_error(name, expr, ptr - expr, 
                "max number of annotations reached");
            return NULL;
        }

        char ch;
        const char *start = ptr;
        for (; (ch = *ptr) && ch != '\n'; ptr ++) { }

        int32_t len = (int32_t)(ptr - start);
        char *annot = ecs_os_malloc_n(char, len + 1);
        ecs_os_memcpy_n(annot, start, char, len);
        annot[len] = '\0';

        state->annot[state->annot_count] = annot;
        state->annot_count ++;

        ptr = plecs_parse_fluff(ptr);
    } while (ptr[0] == '@');

    return ptr;
}

static
void plecs_clear_annotations(
    plecs_state_t *state)
{
    int32_t i, count = state->annot_count;
    for (i = 0; i < count; i ++) {
        ecs_os_free(state->annot[i]);
    }
    state->annot_count = 0;
}

static
const char* plecs_parse_stmt(
    ecs_world_t *world,
    const char *name,
    const char *expr,
    const char *ptr,
    plecs_state_t *state)
{
    state->assign_stmt = false;
    state->scope_assign_stmt = false;
    state->isa_stmt = false;
    state->with_stmt = false;
    state->decl_stmt = false;
    state->var_stmt = false;
    state->last_subject = 0;
    state->last_predicate = 0;
    state->last_object = 0;
    state->assign_to = 0;
    state->last_assign_id = 0;

    plecs_clear_annotations(state);

    ptr = plecs_parse_fluff(ptr);

    char ch = ptr[0];

    if (!ch) {
        goto done;
    } else if (ch == '{') {
        ptr = plecs_parse_fluff(ptr + 1);
        goto scope_open;
    } else if (ch == '}') {
        goto scope_close;
    } else if (ch == '-') {
        ptr = plecs_parse_fluff(ptr + 1);
        state->assign_to = ecs_get_scope(world);
        state->scope_assign_stmt = true;
        goto assign_stmt;
    } else if (ch == '@') {
        ptr = plecs_parse_annotation(name, expr, ptr, state);
        if (!ptr) goto error;
        goto term_expr;
    } else if (!ecs_os_strncmp(ptr, TOK_USING " ", 5)) {
        ptr = plecs_parse_using_stmt(world, name, expr, ptr, state);
        if (!ptr) goto error;
        goto done;
    } else if (!ecs_os_strncmp(ptr, TOK_MODULE " ", 6)) {
        ptr = plecs_parse_module_stmt(world, name, expr, ptr, state);
        if (!ptr) goto error;
        goto done;
    } else if (!ecs_os_strncmp(ptr, TOK_WITH " ", 5)) {
        ptr = plecs_parse_with_stmt(name, expr, ptr, state);
        if (!ptr) goto error;
        goto term_expr;
    } else if (!ecs_os_strncmp(ptr, TOK_CONST " ", 6)) {
        ptr = plecs_parse_const_stmt(world, name, expr, ptr, state);
        if (!ptr) goto error;
        goto assign_expr;
    } else if (!ecs_os_strncmp(ptr, TOK_ASSEMBLY " ", 9)) {
        ptr = plecs_parse_assembly_stmt(name, expr, ptr, state);
        if (!ptr) goto error;
        goto decl_stmt;
    } else if (!ecs_os_strncmp(ptr, TOK_PROP " ", 5)) {
        ptr = plecs_parse_prop_stmt(world, name, expr, ptr, state);
        if (!ptr) goto error;
        goto assign_expr;
    } else {
        goto term_expr;
    }

term_expr:
    if (!ptr[0]) {
        goto done;
    }

    if (ptr[0] == '$' && !isspace(ptr[1])) {
        if (state->with_stmt) {
            ptr = plecs_parse_assign_with_var(name, expr, ptr, state);
            if (!ptr) {
                return NULL;
            }
        } else if (!state->var_stmt) {
            goto assign_var_as_component;
        }
    } else if (!(ptr = plecs_parse_plecs_term(world, name, ptr, ptr, state))) {
        goto error;
    }

    const char *tptr = ecs_parse_ws(ptr);
    if (flecs_isident(tptr[0])) {
        if (state->decl_stmt) {
            ecs_parser_error(name, expr, (ptr - expr), 
                "unexpected ' ' in declaration statement");
            goto error;
        }
        ptr = tptr;
        goto decl_stmt;
    }

next_term:
    ptr = plecs_parse_fluff(ptr);

    if (ptr[0] == ':' && ptr[1] == '-') {
        ptr = plecs_parse_fluff(ptr + 2);
        goto assign_stmt;
    } else if (ptr[0] == ':') {
        ptr = plecs_parse_fluff(ptr + 1);
        goto inherit_stmt;
    } else if (ptr[0] == ',') {
        ptr = plecs_parse_fluff(ptr + 1);
        goto term_expr;
    } else if (ptr[0] == '{') {
        if (state->assign_stmt) {
            goto assign_expr;
        } else if (state->with_stmt && !isspace(ptr[-1])) {
            /* If this is a { in a with statement which directly follows a
                * non-whitespace character, the with id has a value */
            ptr = plecs_parse_assign_with_stmt(world, name, expr, ptr, state);
            if (!ptr) {
                goto error;
            }

            goto next_term;
        } else {
            ptr = plecs_parse_fluff(ptr + 1);
            goto scope_open;
        }
    }

    state->assign_stmt = false;
    goto done;

decl_stmt:
    state->decl_stmt = true;
    goto term_expr;

inherit_stmt:
    ptr = plecs_parse_inherit_stmt(name, expr, ptr, state);
    if (!ptr) goto error;

    /* Expect base identifier */
    goto term_expr;

assign_stmt:
    ptr = plecs_parse_assign_stmt(world, name, expr, ptr, state);
    if (!ptr) goto error;

    ptr = plecs_parse_fluff(ptr);

    /* Assignment without a preceding component */
    if (ptr[0] == '{') {
        goto assign_expr;
    }

    /* Expect component identifiers */
    goto term_expr;

assign_expr:
    ptr = plecs_parse_assign_expr(world, name, expr, ptr, state, NULL);
    if (!ptr) goto error;

    ptr = plecs_parse_fluff(ptr);
    if (ptr[0] == ',') {
        ptr ++;
        goto term_expr;
    } else if (ptr[0] == '{') {
        if (state->var_stmt) {
            ecs_expr_var_t *var = ecs_vars_lookup(&state->vars, state->var_name);
            if (var && var->value.type == ecs_id(ecs_entity_t)) {
                ecs_assert(var->value.ptr != NULL, ECS_INTERNAL_ERROR, NULL);
                /* The code contained an entity{...} variable assignment, use
                 * the assigned entity id as type for parsing the expression */
                state->last_assign_id = *(ecs_entity_t*)var->value.ptr;
                ptr = plecs_parse_assign_expr(world, name, expr, ptr, state, var);
                goto done;
            }
        }
        ecs_parser_error(name, expr, (ptr - expr), 
            "unexpected '{' after assignment");
        goto error;
    }

    state->assign_stmt = false;
    state->assign_to = 0;
    goto done;

assign_var_as_component: {
    ptr = plecs_parse_var_as_component(world, name, expr, ptr, state);
    if (!ptr) {
        goto error;
    }
    state->assign_stmt = false;
    state->assign_to = 0;
    goto done;
}

scope_open:
    ptr = plecs_parse_scope_open(world, name, expr, ptr, state);
    if (!ptr) goto error;
    goto done;

scope_close:
    ptr = plecs_parse_scope_close(world, name, expr, ptr, state);
    if (!ptr) goto error;
    goto done;

done:
    return ptr;
error:
    return NULL;
}

static
int flecs_plecs_parse(
    ecs_world_t *world,
    const char *name,
    const char *expr,
    ecs_vars_t *vars,
    ecs_entity_t script,
    ecs_entity_t instance)
{
    const char *ptr = expr;
    ecs_term_t term = {0};
    plecs_state_t state = {0};

    if (!expr) {
        return 0;
    }

    state.scope[0] = 0;
    ecs_entity_t prev_scope = ecs_set_scope(world, 0);
    ecs_entity_t prev_with = ecs_set_with(world, 0);

    if (ECS_IS_PAIR(prev_with) && ECS_PAIR_FIRST(prev_with) == EcsChildOf) {
        ecs_set_scope(world, ECS_PAIR_SECOND(prev_with));
        state.scope[0] = ecs_pair_second(world, prev_with);
    } else {
        state.global_with = prev_with;
    }

    ecs_vars_init(world, &state.vars);

    if (script) {
        const EcsScript *s = ecs_get(world, script, EcsScript);
        if (!s) {
            ecs_err("%s: provided script entity is not a script", name);
            goto error;
        }
        if (s && ecs_has(world, script, EcsStruct)) {
            state.assembly = script;
            state.assembly_instance = true;

            if (s->using_.count) {
                ecs_os_memcpy_n(state.using, s->using_.array, 
                    ecs_entity_t, s->using_.count);
                state.using_frame = s->using_.count;
                state.using_frames[0] = s->using_.count;
            }

            if (instance) {
                ecs_set_scope(world, instance);
            }
        }
    }

    if (vars) {
        state.vars.root.parent = vars->cur;
    }

    do {
        expr = ptr = plecs_parse_stmt(world, name, expr, ptr, &state);
        if (!ptr) {
            goto error;
        }

        if (!ptr[0]) {
            break; /* End of expression */
        }
    } while (true);

    ecs_set_scope(world, prev_scope);
    ecs_set_with(world, prev_with);
    plecs_clear_annotations(&state);

    if (state.is_module) {
        state.sp --;
    }

    if (state.sp != 0) {
        ecs_parser_error(name, expr, 0, "missing end of scope");
        goto error;
    }

    if (state.assign_stmt) {
        ecs_parser_error(name, expr, 0, "unfinished assignment");
        goto error;
    }

    if (state.errors) {
        goto error;
    }

    ecs_vars_fini(&state.vars);

    return 0;
error:
    ecs_vars_fini(&state.vars);
    ecs_set_scope(world, state.scope[0]);
    ecs_set_with(world, prev_with);
    ecs_term_fini(&term);
    return -1;
}

int ecs_plecs_from_str(
    ecs_world_t *world,
    const char *name,
    const char *expr)
{
    return flecs_plecs_parse(world, name, expr, NULL, 0, 0);
}

static
char* flecs_load_from_file(
    const char *filename)
{
    FILE* file;
    char* content = NULL;
    int32_t bytes;
    size_t size;

    /* Open file for reading */
    ecs_os_fopen(&file, filename, "r");
    if (!file) {
        ecs_err("%s (%s)", ecs_os_strerror(errno), filename);
        goto error;
    }

    /* Determine file size */
    fseek(file, 0 , SEEK_END);
    bytes = (int32_t)ftell(file);
    if (bytes == -1) {
        goto error;
    }
    rewind(file);

    /* Load contents in memory */
    content = ecs_os_malloc(bytes + 1);
    size = (size_t)bytes;
    if (!(size = fread(content, 1, size, file)) && bytes) {
        ecs_err("%s: read zero bytes instead of %d", filename, size);
        ecs_os_free(content);
        content = NULL;
        goto error;
    } else {
        content[size] = '\0';
    }

    fclose(file);

    return content;
error:
    ecs_os_free(content);
    return NULL;
}

int ecs_plecs_from_file(
    ecs_world_t *world,
    const char *filename) 
{
    char *script = flecs_load_from_file(filename);
    if (!script) {
        return -1;
    }

    int result = ecs_plecs_from_str(world, filename, script);
    ecs_os_free(script);
    return result;
}

static
ecs_id_t flecs_script_tag(
    ecs_entity_t script,
    ecs_entity_t instance)
{
    if (!instance) {
        return ecs_pair_t(EcsScript, script);
    } else {
        return ecs_pair(EcsChildOf, instance);
    }
}

void ecs_script_clear(
    ecs_world_t *world,
    ecs_entity_t script,
    ecs_entity_t instance)
{
    ecs_delete_with(world, flecs_script_tag(script, instance));
}

int ecs_script_update(
    ecs_world_t *world,
    ecs_entity_t e,
    ecs_entity_t instance,
    const char *script,
    ecs_vars_t *vars)
{
    ecs_assert(world != NULL, ECS_INTERNAL_ERROR, NULL);
    ecs_assert(script != NULL, ECS_INTERNAL_ERROR, NULL);

    int result = 0;
    bool is_defer = ecs_is_deferred(world);
    ecs_suspend_readonly_state_t srs;
    ecs_world_t *real_world = NULL;
    if (is_defer) {
        ecs_assert(ecs_poly_is(world, ecs_world_t), ECS_INTERNAL_ERROR, NULL);
        real_world = flecs_suspend_readonly(world, &srs);
        ecs_assert(real_world != NULL, ECS_INTERNAL_ERROR, NULL);
    }

    ecs_script_clear(world, e, instance);

    EcsScript *s = ecs_get_mut(world, e, EcsScript);
    if (!s->script || ecs_os_strcmp(s->script, script)) {
        s->script = ecs_os_strdup(script);
        ecs_modified(world, e, EcsScript);
    }

    ecs_entity_t prev = ecs_set_with(world, flecs_script_tag(e, instance));
    if (flecs_plecs_parse(world, ecs_get_name(world, e), script, vars, e, instance)) {
        ecs_delete_with(world, ecs_pair_t(EcsScript, e));
        result = -1;
    }
    ecs_set_with(world, prev);

    if (is_defer) {
        flecs_resume_readonly(real_world, &srs);
    }

    return result;
}

ecs_entity_t ecs_script_init(
    ecs_world_t *world,
    const ecs_script_desc_t *desc)
{
    const char *script = NULL;
    ecs_entity_t e = desc->entity;
    
    ecs_check(world != NULL, ECS_INTERNAL_ERROR, NULL);
    ecs_check(desc != NULL, ECS_INTERNAL_ERROR, NULL);

    if (!e) {
        if (desc->filename) {
            e = ecs_new_from_path_w_sep(world, 0, desc->filename, "/", NULL);
        } else {
            e = ecs_new_id(world);
        }
    }

    script = desc->str;
    if (!script && desc->filename) {
        script = flecs_load_from_file(desc->filename);
        if (!script) {
            goto error;
        }
    }

    if (ecs_script_update(world, e, 0, script, NULL)) {
        goto error;
    }

    if (script != desc->str) {
        /* Safe cast, only happens when script is loaded from file */
        ecs_os_free((char*)script);
    }

    return e;
error:
    if (script != desc->str) {
        /* Safe cast, only happens when script is loaded from file */
        ecs_os_free((char*)script);
    }
    if (!desc->entity) {
        ecs_delete(world, e);
    }
    return 0;
}

void FlecsScriptImport(
    ecs_world_t *world)
{
    ECS_MODULE(world, FlecsScript);
    ECS_IMPORT(world, FlecsMeta);

    ecs_set_name_prefix(world, "Ecs");
    ECS_COMPONENT_DEFINE(world, EcsScript);

    ecs_set_hooks(world, EcsScript, {
        .ctor = ecs_default_ctor,
        .move = ecs_move(EcsScript),
        .dtor = ecs_dtor(EcsScript)
    });

    ecs_add_id(world, ecs_id(EcsScript), EcsTag);

    ecs_struct(world, {
        .entity = ecs_id(EcsScript),
        .members = {
            { .name = "using", .type = ecs_vector(world, { 
                    .entity = ecs_entity(world, { .name = "UsingVector" }),
                    .type = ecs_id(ecs_entity_t)
                }),
                .count = 0
            },
            { .name = "script", .type = ecs_id(ecs_string_t), .count = 0 }
        }
    });
}

#endif

/**
 * @file addons/journal.c
 * @brief Journal addon.
 */


#ifdef FLECS_JOURNAL

static
char* flecs_journal_entitystr(
    ecs_world_t *world,
    ecs_entity_t entity)
{
    char *path;
    const char *_path = ecs_get_symbol(world, entity);
    if (_path && !strchr(_path, '.')) {
        path = ecs_asprintf("#[blue]%s", _path);
    } else {
        uint32_t gen = entity >> 32;
        if (gen) {
            path = ecs_asprintf("#[normal]_%u_%u", (uint32_t)entity, gen);
        } else {
            path = ecs_asprintf("#[normal]_%u", (uint32_t)entity);
        }
    }
    return path;
}

static
char* flecs_journal_idstr(
    ecs_world_t *world,
    ecs_id_t id)
{
    if (ECS_IS_PAIR(id)) {
        char *first_path = flecs_journal_entitystr(world, 
            ecs_pair_first(world, id));
        char *second_path = flecs_journal_entitystr(world, 
            ecs_pair_second(world, id));
        char *result = ecs_asprintf("#[cyan]ecs_pair#[normal](%s, %s)",
            first_path, second_path);
        ecs_os_free(first_path);
        ecs_os_free(second_path);
        return result;
    } else if (!(id & ECS_ID_FLAGS_MASK)) {
        return flecs_journal_entitystr(world, id);
    } else {
        return ecs_id_str(world, id);
    }
}

static int flecs_journal_sp = 0;

void flecs_journal_begin(
    ecs_world_t *world,
    ecs_journal_kind_t kind,
    ecs_entity_t entity,
    ecs_type_t *add,
    ecs_type_t *remove)
{
    flecs_journal_sp ++;

    if (ecs_os_api.log_level_ < FLECS_JOURNAL_LOG_LEVEL) {
        return;
    }

    char *path = NULL; 
    char *var_id = NULL; 
    if (entity) {
        path = ecs_get_fullpath(world, entity);
        var_id = flecs_journal_entitystr(world, entity);
    }

    if (kind == EcsJournalNew) {
        ecs_print(4, "#[magenta]#ifndef #[normal]_var_%s", var_id);
        ecs_print(4, "#[magenta]#define #[normal]_var_%s", var_id);
        ecs_print(4, "#[green]ecs_entity_t %s;", var_id);
        ecs_print(4, "#[magenta]#endif");
        ecs_print(4, "%s = #[cyan]ecs_new_id#[reset](world); "
            "#[grey] // %s = new()", var_id, path);
    }
    if (add) {
        for (int i = 0; i < add->count; i ++) {
            char *jidstr = flecs_journal_idstr(world, add->array[i]);
            char *idstr = ecs_id_str(world, add->array[i]);
            ecs_print(4, "#[cyan]ecs_add_id#[reset](world, %s, %s); "
                "#[grey] // add(%s, %s)", var_id, jidstr, 
                    path, idstr);
            ecs_os_free(idstr);
            ecs_os_free(jidstr);
        }
    }
    if (remove) {
        for (int i = 0; i < remove->count; i ++) {
            char *jidstr = flecs_journal_idstr(world, remove->array[i]);
            char *idstr = ecs_id_str(world, remove->array[i]);
            ecs_print(4, "#[cyan]ecs_remove_id#[reset](world, %s, %s); "
                "#[grey] // remove(%s, %s)", var_id, jidstr, 
                    path, idstr);
            ecs_os_free(idstr);
            ecs_os_free(jidstr);
        }
    }
    if (kind == EcsJournalClear) {
        ecs_print(4, "#[cyan]ecs_clear#[reset](world, %s); "
            "#[grey] // clear(%s)", var_id, path);
    } else if (kind == EcsJournalDelete) {
        ecs_print(4, "#[cyan]ecs_delete#[reset](world, %s); "
            "#[grey] // delete(%s)", var_id, path);
    } else if (kind == EcsJournalDeleteWith) {
        ecs_print(4, "#[cyan]ecs_delete_with#[reset](world, %s); "
            "#[grey] // delete_with(%s)", var_id, path);
    } else if (kind == EcsJournalRemoveAll) {
        ecs_print(4, "#[cyan]ecs_remove_all#[reset](world, %s); "
            "#[grey] // remove_all(%s)", var_id, path);
    } else if (kind == EcsJournalTableEvents) {
        ecs_print(4, "#[cyan]ecs_run_aperiodic#[reset](world, "
            "EcsAperiodicEmptyTables);");
    }
    ecs_os_free(var_id);
    ecs_os_free(path);
    ecs_log_push();
}

void flecs_journal_end(void) {
    flecs_journal_sp --;
    ecs_assert(flecs_journal_sp >= 0, ECS_INTERNAL_ERROR, NULL);
    ecs_log_pop();
}

#endif

/**
 * @file addons/module.c
 * @brief Module addon.
 */


#ifdef FLECS_MODULE

#include <ctype.h>

char* ecs_module_path_from_c(
    const char *c_name)
{
    ecs_strbuf_t str = ECS_STRBUF_INIT;
    const char *ptr;
    char ch;

    for (ptr = c_name; (ch = *ptr); ptr++) {
        if (isupper(ch)) {
            ch = flecs_ito(char, tolower(ch));
            if (ptr != c_name) {
                ecs_strbuf_appendstrn(&str, ".", 1);
            }
        }

        ecs_strbuf_appendstrn(&str, &ch, 1);
    }

    return ecs_strbuf_get(&str);
}

ecs_entity_t ecs_import(
    ecs_world_t *world,
    ecs_module_action_t module,
    const char *module_name)
{
    ecs_check(!(world->flags & EcsWorldReadonly), 
        ECS_INVALID_WHILE_READONLY, NULL);

    ecs_entity_t old_scope = ecs_set_scope(world, 0);
    const char *old_name_prefix = world->info.name_prefix;

    char *path = ecs_module_path_from_c(module_name);
    ecs_entity_t e = ecs_lookup_fullpath(world, path);
    ecs_os_free(path);
    
    if (!e) {
        ecs_trace("#[magenta]import#[reset] %s", module_name);
        ecs_log_push();

        /* Load module */
        module(world);

        /* Lookup module entity (must be registered by module) */
        e = ecs_lookup_fullpath(world, module_name);
        ecs_check(e != 0, ECS_MODULE_UNDEFINED, module_name);

        ecs_log_pop();
    }

    /* Restore to previous state */
    ecs_set_scope(world, old_scope);
    world->info.name_prefix = old_name_prefix;

    return e;
error:
    return 0;
}

ecs_entity_t ecs_import_c(
    ecs_world_t *world,
    ecs_module_action_t module,
    const char *c_name)
{
    char *name = ecs_module_path_from_c(c_name);
    ecs_entity_t e = ecs_import(world, module, name);
    ecs_os_free(name);
    return e;
}

ecs_entity_t ecs_import_from_library(
    ecs_world_t *world,
    const char *library_name,
    const char *module_name)
{
    ecs_check(library_name != NULL, ECS_INVALID_PARAMETER, NULL);

    char *import_func = (char*)module_name; /* safe */
    char *module = (char*)module_name;

    if (!ecs_os_has_modules() || !ecs_os_has_dl()) {
        ecs_err(
            "library loading not supported, set module_to_dl, dlopen, dlclose "
            "and dlproc os API callbacks first");
        return 0;
    }

    /* If no module name is specified, try default naming convention for loading
     * the main module from the library */
    if (!import_func) {
        import_func = ecs_os_malloc(ecs_os_strlen(library_name) + ECS_SIZEOF("Import"));
        ecs_assert(import_func != NULL, ECS_OUT_OF_MEMORY, NULL);
        
        const char *ptr;
        char ch, *bptr = import_func;
        bool capitalize = true;
        for (ptr = library_name; (ch = *ptr); ptr ++) {
            if (ch == '.') {
                capitalize = true;
            } else {
                if (capitalize) {
                    *bptr = flecs_ito(char, toupper(ch));
                    bptr ++;
                    capitalize = false;
                } else {
                    *bptr = flecs_ito(char, tolower(ch));
                    bptr ++;
                }
            }
        }

        *bptr = '\0';

        module = ecs_os_strdup(import_func);
        ecs_assert(module != NULL, ECS_OUT_OF_MEMORY, NULL);

        ecs_os_strcat(bptr, "Import");
    }

    char *library_filename = ecs_os_module_to_dl(library_name);
    if (!library_filename) {
        ecs_err("failed to find library file for '%s'", library_name);
        if (module != module_name) {
            ecs_os_free(module);
        }
        return 0;
    } else {
        ecs_trace("found file '%s' for library '%s'", 
            library_filename, library_name);
    }

    ecs_os_dl_t dl = ecs_os_dlopen(library_filename);
    if (!dl) {
        ecs_err("failed to load library '%s' ('%s')", 
            library_name, library_filename);
        
        ecs_os_free(library_filename);

        if (module != module_name) {
            ecs_os_free(module);
        }    

        return 0;
    } else {
        ecs_trace("library '%s' ('%s') loaded", 
            library_name, library_filename);
    }

    ecs_module_action_t action = (ecs_module_action_t)
        ecs_os_dlproc(dl, import_func);
    if (!action) {
        ecs_err("failed to load import function %s from library %s",
            import_func, library_name);
        ecs_os_free(library_filename);
        ecs_os_dlclose(dl);            
        return 0;
    } else {
        ecs_trace("found import function '%s' in library '%s' for module '%s'",
            import_func, library_name, module);
    }

    /* Do not free id, as it will be stored as the component identifier */
    ecs_entity_t result = ecs_import(world, action, module);

    if (import_func != module_name) {
        ecs_os_free(import_func);
    }

    if (module != module_name) {
        ecs_os_free(module);
    }

    ecs_os_free(library_filename);

    return result;
error:
    return 0;
}

ecs_entity_t ecs_module_init(
    ecs_world_t *world,
    const char *c_name,
    const ecs_component_desc_t *desc)
{
    ecs_check(desc != NULL, ECS_INVALID_PARAMETER, NULL);
    ecs_poly_assert(world, ecs_world_t);

    ecs_entity_t old_scope = ecs_set_scope(world, 0);

    ecs_entity_t e = desc->entity;
    if (!e) {
        char *module_path = ecs_module_path_from_c(c_name);
        e = ecs_new_from_fullpath(world, module_path);
        ecs_set_symbol(world, e, module_path);
        ecs_os_free(module_path);
    } else if (!ecs_exists(world, e)) {
        char *module_path = ecs_module_path_from_c(c_name);
        ecs_ensure(world, e);
        ecs_add_fullpath(world, e, module_path);
        ecs_set_symbol(world, e, module_path);
        ecs_os_free(module_path);
    }
    
    ecs_add_id(world, e, EcsModule);

    ecs_component_desc_t private_desc = *desc;
    private_desc.entity = e;

    if (desc->type.size) {
        ecs_entity_t result = ecs_component_init(world, &private_desc);
        ecs_assert(result != 0, ECS_INTERNAL_ERROR, NULL);
        ecs_assert(result == e, ECS_INTERNAL_ERROR, NULL);
        (void)result;
    }

    ecs_set_scope(world, old_scope);

    return e;
error:
    return 0;
}

#endif

/**
 * @file addons/metrics.c
 * @brief Metrics addon.
 */


#ifdef FLECS_METRICS

/* Public components */
ECS_COMPONENT_DECLARE(FlecsMetrics);
ECS_TAG_DECLARE(EcsMetricInstance);
ECS_COMPONENT_DECLARE(EcsMetricValue);
ECS_COMPONENT_DECLARE(EcsMetricSource);
ECS_TAG_DECLARE(EcsMetric);
ECS_TAG_DECLARE(EcsCounter);
ECS_TAG_DECLARE(EcsCounterIncrement);
ECS_TAG_DECLARE(EcsCounterId);
ECS_TAG_DECLARE(EcsGauge);

/* Internal components */
ECS_COMPONENT_DECLARE(EcsMetricMember);
ECS_COMPONENT_DECLARE(EcsMetricId);
ECS_COMPONENT_DECLARE(EcsMetricOneOf);
ECS_COMPONENT_DECLARE(EcsMetricCountIds);
ECS_COMPONENT_DECLARE(EcsMetricCountTargets);
ECS_COMPONENT_DECLARE(EcsMetricMemberInstance);
ECS_COMPONENT_DECLARE(EcsMetricIdInstance);
ECS_COMPONENT_DECLARE(EcsMetricOneOfInstance);

/** Context for metric */
typedef struct {
    ecs_entity_t metric;              /**< Metric entity */
    ecs_entity_t kind;                /**< Metric kind (gauge, counter) */
} ecs_metric_ctx_t;

/** Context for metric that monitors member */
typedef struct {
    ecs_metric_ctx_t metric;
    ecs_primitive_kind_t type_kind;  /**< Primitive type kind of member */
    uint16_t offset;                 /**< Offset of member in component */
} ecs_member_metric_ctx_t;

/** Context for metric that monitors whether entity has id */
typedef struct {
    ecs_metric_ctx_t metric;
    ecs_id_record_t *idr;            /**< Id record for monitored component */
} ecs_id_metric_ctx_t;

/** Context for metric that monitors whether entity has pair target */
typedef struct {
    ecs_metric_ctx_t metric;
    ecs_id_record_t *idr;            /**< Id record for monitored component */
    ecs_size_t size;                 /**< Size of metric type */
    ecs_map_t target_offset;         /**< Pair target to metric type offset */
} ecs_oneof_metric_ctx_t;

/** Context for metric that monitors how many entities have a pair target */
typedef struct {
    ecs_metric_ctx_t metric;
    ecs_id_record_t *idr;            /**< Id record for monitored component */
    ecs_map_t targets;               /**< Map of counters for each target */
} ecs_count_targets_metric_ctx_t;

/** Stores context shared for all instances of member metric */
typedef struct {
    ecs_member_metric_ctx_t *ctx;
} EcsMetricMember;

/** Stores context shared for all instances of id metric */
typedef struct {
    ecs_id_metric_ctx_t *ctx;
} EcsMetricId;

/** Stores context shared for all instances of oneof metric */
typedef struct {
    ecs_oneof_metric_ctx_t *ctx;
} EcsMetricOneOf;

/** Stores context shared for all instances of id counter metric */
typedef struct {
    ecs_id_t id;
} EcsMetricCountIds;

/** Stores context shared for all instances of target counter metric */
typedef struct {
    ecs_count_targets_metric_ctx_t *ctx;
} EcsMetricCountTargets;

/** Instance of member metric */
typedef struct {
    ecs_ref_t ref;
    ecs_member_metric_ctx_t *ctx;
} EcsMetricMemberInstance;

/** Instance of id metric */
typedef struct {
    ecs_record_t *r;
    ecs_id_metric_ctx_t *ctx;
} EcsMetricIdInstance;

/** Instance of oneof metric */
typedef struct {
    ecs_record_t *r;
    ecs_oneof_metric_ctx_t *ctx;
} EcsMetricOneOfInstance;

/** Component lifecycle */

static ECS_DTOR(EcsMetricMember, ptr, {
    ecs_os_free(ptr->ctx);
})

static ECS_MOVE(EcsMetricMember, dst, src, {
    *dst = *src;
    src->ctx = NULL;
})

static ECS_DTOR(EcsMetricId, ptr, {
    ecs_os_free(ptr->ctx);
})

static ECS_MOVE(EcsMetricId, dst, src, {
    *dst = *src;
    src->ctx = NULL;
})

static ECS_DTOR(EcsMetricOneOf, ptr, {
    if (ptr->ctx) {
        ecs_map_fini(&ptr->ctx->target_offset);
        ecs_os_free(ptr->ctx);
    }
})

static ECS_MOVE(EcsMetricOneOf, dst, src, {
    *dst = *src;
    src->ctx = NULL;
})

static ECS_DTOR(EcsMetricCountTargets, ptr, {
    if (ptr->ctx) {
        ecs_map_fini(&ptr->ctx->targets);
        ecs_os_free(ptr->ctx);
    }
})

static ECS_MOVE(EcsMetricCountTargets, dst, src, {
    *dst = *src;
    src->ctx = NULL;
})

/** Observer used for creating new instances of member metric */
static void flecs_metrics_on_member_metric(ecs_iter_t *it) {
    ecs_world_t *world = it->world;
    ecs_member_metric_ctx_t *ctx = it->ctx;
    ecs_id_t id = ecs_field_id(it, 1);

    int32_t i, count = it->count;
    for (i = 0; i < count; i ++) {
        ecs_entity_t e = it->entities[i];
        ecs_entity_t m = ecs_new_w_pair(world, EcsChildOf, ctx->metric.metric);

        EcsMetricMemberInstance *src = ecs_emplace(
            world, m, EcsMetricMemberInstance);
        src->ref = ecs_ref_init_id(world, e, id);
        src->ctx = ctx;
        ecs_modified(world, m, EcsMetricMemberInstance);
        ecs_set(world, m, EcsMetricValue, { 0 });
        ecs_set(world, m, EcsMetricSource, { e });
        ecs_add(world, m, EcsMetricInstance);
        ecs_add_pair(world, m, EcsMetric, ctx->metric.kind);
    }
}

/** Observer used for creating new instances of id metric */
static void flecs_metrics_on_id_metric(ecs_iter_t *it) {
    ecs_world_t *world = it->world;
    ecs_id_metric_ctx_t *ctx = it->ctx;

    int32_t i, count = it->count;
    for (i = 0; i < count; i ++) {
        ecs_entity_t e = it->entities[i];
        ecs_entity_t m = ecs_new_w_pair(world, EcsChildOf, ctx->metric.metric);

        EcsMetricIdInstance *src = ecs_emplace(world, m, EcsMetricIdInstance);
        src->r = ecs_record_find(world, e);
        src->ctx = ctx;
        ecs_modified(world, m, EcsMetricIdInstance);
        ecs_set(world, m, EcsMetricValue, { 0 });
        ecs_set(world, m, EcsMetricSource, { e });
        ecs_add(world, m, EcsMetricInstance);
        ecs_add_pair(world, m, EcsMetric, ctx->metric.kind);
    }
}

/** Observer used for creating new instances of oneof metric */
static void flecs_metrics_on_oneof_metric(ecs_iter_t *it) {
    if (it->event == EcsOnRemove) {
        return;
    }

    ecs_world_t *world = it->world;
    ecs_oneof_metric_ctx_t *ctx = it->ctx;

    int32_t i, count = it->count;
    for (i = 0; i < count; i ++) {
        ecs_entity_t e = it->entities[i];
        ecs_entity_t m = ecs_new_w_pair(world, EcsChildOf, ctx->metric.metric);

        EcsMetricOneOfInstance *src = ecs_emplace(world, m, EcsMetricOneOfInstance);
        src->r = ecs_record_find(world, e);
        src->ctx = ctx;
        ecs_modified(world, m, EcsMetricOneOfInstance);
        ecs_add_pair(world, m, ctx->metric.metric, ecs_id(EcsMetricValue));
        ecs_set(world, m, EcsMetricSource, { e });
        ecs_add(world, m, EcsMetricInstance);
        ecs_add_pair(world, m, EcsMetric, ctx->metric.kind);
    }
}

/** Set doc name of metric instance to name of source entity */
#ifdef FLECS_DOC
static void SetMetricDocName(ecs_iter_t *it) {
    ecs_world_t *world = it->world;
    EcsMetricSource *src = ecs_field(it, EcsMetricSource, 1);

    int32_t i, count = it->count;
    for (i = 0; i < count; i ++) {
        ecs_entity_t src_e = src[i].entity;
        const char *name = ecs_get_name(world, src_e);
        if (name) {
            ecs_doc_set_name(world, it->entities[i], name);
        }
    }
}
#endif

/** Delete metric instances for entities that are no longer alive */
static void ClearMetricInstance(ecs_iter_t *it) {
    ecs_world_t *world = it->world;
    EcsMetricSource *src = ecs_field(it, EcsMetricSource, 1);

    int32_t i, count = it->count;
    for (i = 0; i < count; i ++) {
        ecs_entity_t src_e = src[i].entity;
        if (!ecs_is_alive(world, src_e)) {
            ecs_delete(world, it->entities[i]);
        }
    }
}

/** Update member metric */
static void UpdateMemberInstance(ecs_iter_t *it, bool counter) {
    ecs_world_t *world = it->real_world;
    EcsMetricValue *m = ecs_field(it, EcsMetricValue, 1);
    EcsMetricMemberInstance *mi = ecs_field(it, EcsMetricMemberInstance, 2);
    ecs_ftime_t dt = it->delta_time;

    int32_t i, count = it->count;
    for (i = 0; i < count; i ++) {
        ecs_member_metric_ctx_t *ctx = mi[i].ctx;
        ecs_ref_t *ref = &mi[i].ref;
        const void *ptr = ecs_ref_get_id(world, ref, ref->id);
        if (ptr) {
            ptr = ECS_OFFSET(ptr, ctx->offset);
            if (!counter) {
                m[i].value = ecs_meta_ptr_to_float(ctx->type_kind, ptr);
            } else {
                m[i].value += 
                    ecs_meta_ptr_to_float(ctx->type_kind, ptr) * (double)dt;
            }
        } else {
            ecs_delete(it->world, it->entities[i]);
        }
    }
}

static void UpdateGaugeMemberInstance(ecs_iter_t *it) {
    UpdateMemberInstance(it, false);
}

static void UpdateCounterMemberInstance(ecs_iter_t *it) {
    UpdateMemberInstance(it, false);
}

static void UpdateCounterIncrementMemberInstance(ecs_iter_t *it) {
    UpdateMemberInstance(it, true);
}

/** Update id metric */
static void UpdateIdInstance(ecs_iter_t *it, bool counter) {
    ecs_world_t *world = it->real_world;
    EcsMetricValue *m = ecs_field(it, EcsMetricValue, 1);
    EcsMetricIdInstance *mi = ecs_field(it, EcsMetricIdInstance, 2);
    ecs_ftime_t dt = it->delta_time;

    int32_t i, count = it->count;
    for (i = 0; i < count; i ++) {
        ecs_table_t *table = mi[i].r->table;
        if (!table) {
            ecs_delete(it->world, it->entities[i]);
            continue;
        }

        ecs_id_metric_ctx_t *ctx = mi[i].ctx;
        ecs_id_record_t *idr = ctx->idr;
        if (flecs_search_w_idr(world, table, idr->id, NULL, idr) != -1) {
            if (!counter) {
                m[i].value = 1.0;
            } else {
                m[i].value += 1.0 * (double)dt;
            }
        } else {
            ecs_delete(it->world, it->entities[i]);
        }
    }
}

static void UpdateGaugeIdInstance(ecs_iter_t *it) {
    UpdateIdInstance(it, false);
}

static void UpdateCounterIdInstance(ecs_iter_t *it) {
    UpdateIdInstance(it, true);
}

/** Update oneof metric */
static void UpdateOneOfInstance(ecs_iter_t *it, bool counter) {
    ecs_world_t *world = it->real_world;
    void *m = ecs_table_get_column(it->table, it->columns[0] - 1, it->offset);
    EcsMetricOneOfInstance *mi = ecs_field(it, EcsMetricOneOfInstance, 2);
    ecs_ftime_t dt = it->delta_time;

    int32_t i, count = it->count;
    for (i = 0; i < count; i ++) {
        ecs_oneof_metric_ctx_t *ctx = mi[i].ctx;
        ecs_table_t *table = mi[i].r->table;

        double *value = ECS_ELEM(m, ctx->size, i);
        if (!counter) {
            ecs_os_memset(value, 0, ctx->size);
        }

        if (!table) {
            ecs_delete(it->world, it->entities[i]);
            continue;
        }

        ecs_id_record_t *idr = ctx->idr;
        ecs_id_t id;
        if (flecs_search_w_idr(world, table, idr->id, &id, idr) == -1) {
            ecs_delete(it->world, it->entities[i]);
            continue;
        }

        ecs_entity_t tgt = ECS_PAIR_SECOND(id);
        uint64_t *offset = ecs_map_get(&ctx->target_offset, tgt);
        if (!offset) {
            ecs_err("unexpected relationship target for metric");
            continue;
        }

        value = ECS_OFFSET(value, *offset);

        if (!counter) {
            *value = 1.0;
        } else {
            *value += 1.0 * (double)dt;
        }
    }
}

static void UpdateGaugeOneOfInstance(ecs_iter_t *it) {
    UpdateOneOfInstance(it, false);
}

static void UpdateCounterOneOfInstance(ecs_iter_t *it) {
    UpdateOneOfInstance(it, true);
}

static void UpdateCountTargets(ecs_iter_t *it) {
    ecs_world_t *world = it->real_world;
    EcsMetricCountTargets *m = ecs_field(it, EcsMetricCountTargets, 1);

    int32_t i, count = it->count;
    for (i = 0; i < count; i ++) {
        ecs_count_targets_metric_ctx_t *ctx = m[i].ctx;
        ecs_id_record_t *cur = ctx->idr;
        while ((cur = cur->first.next)) {
            ecs_id_t id = cur->id;
            ecs_entity_t *mi = ecs_map_ensure(&ctx->targets, id);
            if (!mi[0]) {
                mi[0] = ecs_new_w_pair(world, EcsChildOf, ctx->metric.metric);
                ecs_entity_t tgt = ecs_pair_second(world, cur->id);
                const char *name = ecs_get_name(world, tgt);
                if (name) {
                    ecs_set_name(world, mi[0], name);
                }

                EcsMetricSource *source = ecs_get_mut(
                    world, mi[0], EcsMetricSource);
                source->entity = tgt;
            }

            EcsMetricValue *value = ecs_get_mut(world, mi[0], EcsMetricValue);
            value->value += (double)ecs_count_id(world, cur->id) * 
                (double)it->delta_system_time;
        }
    }
}

static void UpdateCountIds(ecs_iter_t *it) {
    ecs_world_t *world = it->real_world;
    EcsMetricCountIds *m = ecs_field(it, EcsMetricCountIds, 1);
    EcsMetricValue *v = ecs_field(it, EcsMetricValue, 2);

    int32_t i, count = it->count;
    for (i = 0; i < count; i ++) {
        v[i].value += (double)ecs_count_id(world, m[i].id) * 
            (double)it->delta_system_time;
    }
}

/** Initialize member metric */
static
int flecs_member_metric_init(
    ecs_world_t *world,
    ecs_entity_t metric,
    const ecs_metric_desc_t *desc)
{
    const EcsMember *m = ecs_get(world, desc->member, EcsMember);
    if (!m) {
        char *metric_name = ecs_get_fullpath(world, metric);
        char *member_name = ecs_get_fullpath(world, desc->member);
        ecs_err("entity '%s' provided for metric '%s' is not a member",
            member_name, metric_name);
        ecs_os_free(member_name);
        ecs_os_free(metric_name);
        goto error;
    }

    const EcsPrimitive *p = ecs_get(world, m->type, EcsPrimitive);
    if (!p) {
        char *metric_name = ecs_get_fullpath(world, metric);
        char *member_name = ecs_get_fullpath(world, desc->member);
        ecs_err("member '%s' provided for metric '%s' must have primitive type",
            member_name, metric_name);
        ecs_os_free(member_name);
        ecs_os_free(metric_name);
        goto error;
    }

    ecs_entity_t type = ecs_get_parent(world, desc->member);
    if (!type) {
        char *metric_name = ecs_get_fullpath(world, metric);
        char *member_name = ecs_get_fullpath(world, desc->member);
        ecs_err("member '%s' provided for metric '%s' is not part of a type",
            member_name, metric_name);
        ecs_os_free(member_name);
        ecs_os_free(metric_name);
        goto error;
    }

    const EcsMetaType *mt = ecs_get(world, type, EcsMetaType);
    if (!mt) {
        char *metric_name = ecs_get_fullpath(world, metric);
        char *member_name = ecs_get_fullpath(world, desc->member);
        ecs_err("parent of member '%s' for metric '%s' is not a type",
            member_name, metric_name);
        ecs_os_free(member_name);
        ecs_os_free(metric_name);
        goto error;
    }

    if (mt->kind != EcsStructType) {
        char *metric_name = ecs_get_fullpath(world, metric);
        char *member_name = ecs_get_fullpath(world, desc->member);
        ecs_err("parent of member '%s' for metric '%s' is not a struct",
            member_name, metric_name);
        ecs_os_free(member_name);
        ecs_os_free(metric_name);
        goto error;
    }

    ecs_member_metric_ctx_t *ctx = ecs_os_calloc_t(ecs_member_metric_ctx_t);
    ctx->metric.metric = metric;
    ctx->metric.kind = desc->kind;
    ctx->type_kind = p->kind;
    ctx->offset = flecs_ito(uint16_t, m->offset);

    ecs_observer(world, {
        .entity = metric,
        .events = { EcsOnAdd },
        .filter.terms[0] = {
            .id = type,
            .src.flags = EcsSelf,
            .inout = EcsInOutNone
        },
        .callback = flecs_metrics_on_member_metric,
        .yield_existing = true,
        .ctx = ctx
    });

    ecs_set_pair(world, metric, EcsMetricMember, desc->member, { .ctx = ctx });
    ecs_add_pair(world, metric, EcsMetric, desc->kind);
    ecs_add_id(world, metric, EcsMetric);

    return 0;
error:
    return -1;
}

/** Update id metric */
static
int flecs_id_metric_init(
    ecs_world_t *world,
    ecs_entity_t metric,
    const ecs_metric_desc_t *desc)
{
    ecs_id_metric_ctx_t *ctx = ecs_os_calloc_t(ecs_id_metric_ctx_t);
    ctx->metric.metric = metric;
    ctx->metric.kind = desc->kind;
    ctx->idr = flecs_id_record_ensure(world, desc->id);
    ecs_check(ctx->idr != NULL, ECS_INVALID_PARAMETER, NULL);

    ecs_observer(world, {
        .entity = metric,
        .events = { EcsOnAdd },
        .filter.terms[0] = {
            .id = desc->id,
            .src.flags = EcsSelf,
            .inout = EcsInOutNone
        },
        .callback = flecs_metrics_on_id_metric,
        .yield_existing = true,
        .ctx = ctx
    });

    ecs_set(world, metric, EcsMetricId, { .ctx = ctx });
    ecs_add_pair(world, metric, EcsMetric, desc->kind);
    ecs_add_id(world, metric, EcsMetric);

    return 0;
error:
    return -1;
}

/** Update oneof metric */
static
int flecs_oneof_metric_init(
    ecs_world_t *world,
    ecs_entity_t metric,
    ecs_entity_t scope,
    const ecs_metric_desc_t *desc)
{
    ecs_oneof_metric_ctx_t *ctx = ecs_os_calloc_t(ecs_oneof_metric_ctx_t);
    ctx->metric.metric = metric;
    ctx->metric.kind = desc->kind;
    ctx->idr = flecs_id_record_ensure(world, desc->id);
    ecs_check(ctx->idr != NULL, ECS_INVALID_PARAMETER, NULL);
    ecs_map_init(&ctx->target_offset, NULL);

    /* Add member for each child of oneof to metric, so it can be used as metric
     * instance type that holds values for all targets */
    ecs_iter_t it = ecs_children(world, scope);
    uint64_t offset = 0;
    while (ecs_children_next(&it)) {
        int32_t i, count = it.count;
        for (i = 0; i < count; i ++) {
            ecs_entity_t tgt = it.entities[i];
            const char *name = ecs_get_name(world, tgt);
            if (!name) {
                /* Member must have name */
                continue;
            }

            char *to_snake_case = flecs_to_snake_case(name);

            ecs_entity_t mbr = ecs_entity(world, {
                .name = to_snake_case,
                .add = { ecs_childof(metric) }
            });

            ecs_os_free(to_snake_case);

            ecs_set(world, mbr, EcsMember, {
                .type = ecs_id(ecs_f64_t),
                .unit = EcsSeconds
            });

            /* Truncate upper 32 bits of target so we can lookup the offset
             * with the id we get from the pair */
            ecs_map_ensure(&ctx->target_offset, (uint32_t)tgt)[0] = offset;

            offset += sizeof(double);
        }
    }

    ctx->size = flecs_uto(ecs_size_t, offset);

    ecs_observer(world, {
        .entity = metric,
        .events = { EcsMonitor },
        .filter.terms[0] = {
            .id = desc->id,
            .src.flags = EcsSelf,
            .inout = EcsInOutNone
        },
        .callback = flecs_metrics_on_oneof_metric,
        .yield_existing = true,
        .ctx = ctx
    });

    ecs_set(world, metric, EcsMetricOneOf, { .ctx = ctx });
    ecs_add_pair(world, metric, EcsMetric, desc->kind);
    ecs_add_id(world, metric, EcsMetric);

    return 0;
error:
    return -1;
}

static
int flecs_count_id_targets_metric_init(
    ecs_world_t *world,
    ecs_entity_t metric,
    const ecs_metric_desc_t *desc)
{
    ecs_count_targets_metric_ctx_t *ctx = ecs_os_calloc_t(ecs_count_targets_metric_ctx_t);
    ctx->metric.metric = metric;
    ctx->metric.kind = desc->kind;
    ctx->idr = flecs_id_record_ensure(world, desc->id);
    ecs_check(ctx->idr != NULL, ECS_INVALID_PARAMETER, NULL);
    ecs_map_init(&ctx->targets, NULL);

    ecs_set(world, metric, EcsMetricCountTargets, { .ctx = ctx });
    ecs_add_pair(world, metric, EcsMetric, desc->kind);
    ecs_add_id(world, metric, EcsMetric); 

    return 0;
error:
    return -1;
}

static
int flecs_count_ids_metric_init(
    ecs_world_t *world,
    ecs_entity_t metric,
    const ecs_metric_desc_t *desc)
{
    ecs_set(world, metric, EcsMetricCountIds, { .id = desc->id });
    ecs_set(world, metric, EcsMetricValue, { .value = 0 });
    return 0;
}

ecs_entity_t ecs_metric_init(
    ecs_world_t *world,
    const ecs_metric_desc_t *desc)
{
    ecs_check(desc != NULL, ECS_INVALID_PARAMETER, NULL);
    ecs_check(desc->_canary == 0, ECS_INVALID_PARAMETER, NULL);
    ecs_poly_assert(world, ecs_world_t);

    ecs_entity_t result = desc->entity;
    if (!result) {
        result = ecs_new_id(world);
    }

    ecs_entity_t kind = desc->kind;
    if (!kind) {
        ecs_err("missing metric kind");
        goto error;
    }

    if (kind != EcsGauge && 
        kind != EcsCounter && 
        kind != EcsCounterId &&
        kind != EcsCounterIncrement) 
    {
        ecs_err("invalid metric kind %s", ecs_get_fullpath(world, kind));
        goto error;
    }

    if (kind == EcsCounterIncrement && !desc->member) {
        ecs_err("CounterIncrement can only be used in combination with member");
        goto error;
    }

    if (kind == EcsCounterId && desc->member) {
        ecs_err("CounterIncrement cannot be used in combination with member");
        goto error;
    }

    if (desc->brief) {
#ifdef FLECS_DOC
        ecs_doc_set_brief(world, result, desc->brief);
#else
        ecs_warn("FLECS_DOC is not enabled, ignoring metrics brief");
#endif        
    }

    if (desc->member) {
        if (desc->id) {
            ecs_err("cannot specify both member and id for metric");
            goto error;
        }
        if (flecs_member_metric_init(world, result, desc)) {
            goto error;
        }
    } else if (desc->id) {
        if (desc->targets) {
            if (!ecs_id_is_pair(desc->id)) {
                ecs_err("cannot specify targets for id that is not a pair");
                goto error;
            }
            if (ECS_PAIR_FIRST(desc->id) == EcsWildcard) {
                ecs_err("first element of pair cannot be wildcard with "
                    " targets enabled");
                goto error;
            }
            if (ECS_PAIR_SECOND(desc->id) != EcsWildcard) {
                ecs_err("second element of pair must be wildcard with "
                    " targets enabled");
                goto error;
            }

            if (kind == EcsCounterId) {
                if (flecs_count_id_targets_metric_init(world, result, desc)) {
                    goto error;
                }
            } else {
                ecs_entity_t first = ecs_pair_first(world, desc->id);
                ecs_entity_t scope = flecs_get_oneof(world, first);
                if (!scope) {
                    ecs_err("first element of pair must have OneOf with "
                        " targets enabled");
                    goto error;
                }

                if (flecs_oneof_metric_init(world, result, scope, desc)) {
                    goto error;
                }
            }
        } else {
            if (kind == EcsCounterId) {
                if (flecs_count_ids_metric_init(world, result, desc)) {
                    goto error;
                }
            } else {
                if (flecs_id_metric_init(world, result, desc)) {
                    goto error;
                }
            }
        }
    } else {
        ecs_err("missing source specified for metric");
        goto error;
    }

    return result;
error:
    if (result && result != desc->entity) {
        ecs_delete(world, result);
    }
    return 0;
}

void FlecsMetricsImport(ecs_world_t *world) {
    ECS_MODULE_DEFINE(world, FlecsMetrics);

    ECS_IMPORT(world, FlecsPipeline);
    ECS_IMPORT(world, FlecsMeta);
    ECS_IMPORT(world, FlecsUnits);

    ecs_set_name_prefix(world, "Ecs");
    ECS_TAG_DEFINE(world, EcsMetric);
    ecs_entity_t old_scope = ecs_set_scope(world, EcsMetric);
    ECS_TAG_DEFINE(world, EcsCounter);
    ECS_TAG_DEFINE(world, EcsCounterIncrement);
    ECS_TAG_DEFINE(world, EcsCounterId);
    ECS_TAG_DEFINE(world, EcsGauge);
    ecs_set_scope(world, old_scope);

    ecs_set_name_prefix(world, "EcsMetric");
    ECS_TAG_DEFINE(world, EcsMetricInstance);
    ECS_COMPONENT_DEFINE(world, EcsMetricValue);
    ECS_COMPONENT_DEFINE(world, EcsMetricSource);
    ECS_COMPONENT_DEFINE(world, EcsMetricMemberInstance);
    ECS_COMPONENT_DEFINE(world, EcsMetricIdInstance);
    ECS_COMPONENT_DEFINE(world, EcsMetricOneOfInstance);
    ECS_COMPONENT_DEFINE(world, EcsMetricMember);
    ECS_COMPONENT_DEFINE(world, EcsMetricId);
    ECS_COMPONENT_DEFINE(world, EcsMetricOneOf);
    ECS_COMPONENT_DEFINE(world, EcsMetricCountIds);
    ECS_COMPONENT_DEFINE(world, EcsMetricCountTargets);

    ecs_add_id(world, ecs_id(EcsMetricMemberInstance), EcsPrivate);
    ecs_add_id(world, ecs_id(EcsMetricIdInstance), EcsPrivate);
    ecs_add_id(world, ecs_id(EcsMetricOneOfInstance), EcsPrivate);

    ecs_struct(world, {
        .entity = ecs_id(EcsMetricValue),
        .members = {
            { .name = "value", .type = ecs_id(ecs_f64_t) }
        }
    });

    ecs_struct(world, {
        .entity = ecs_id(EcsMetricSource),
        .members = {
            { .name = "entity", .type = ecs_id(ecs_entity_t) }
        }
    });

    ecs_set_hooks(world, EcsMetricMember, {
        .ctor = ecs_default_ctor,
        .dtor = ecs_dtor(EcsMetricMember),
        .move = ecs_move(EcsMetricMember)
    });

    ecs_set_hooks(world, EcsMetricId, {
        .ctor = ecs_default_ctor,
        .dtor = ecs_dtor(EcsMetricId),
        .move = ecs_move(EcsMetricId)
    });

    ecs_set_hooks(world, EcsMetricOneOf, {
        .ctor = ecs_default_ctor,
        .dtor = ecs_dtor(EcsMetricOneOf),
        .move = ecs_move(EcsMetricOneOf)
    });

    ecs_set_hooks(world, EcsMetricCountTargets, {
        .ctor = ecs_default_ctor,
        .dtor = ecs_dtor(EcsMetricCountTargets),
        .move = ecs_move(EcsMetricCountTargets)
    });

    ecs_add_id(world, EcsMetric, EcsOneOf);

#ifdef FLECS_DOC
    ECS_OBSERVER(world, SetMetricDocName, EcsOnSet, EcsMetricSource);
#endif

    ECS_SYSTEM(world, ClearMetricInstance, EcsPreStore,
        [in] Source);

    ECS_SYSTEM(world, UpdateGaugeMemberInstance, EcsPreStore, 
        [out]  Value, 
        [in]   MemberInstance,
        [none] (Metric, Gauge));

    ECS_SYSTEM(world, UpdateCounterMemberInstance, EcsPreStore, 
        [out]  Value, 
        [in]   MemberInstance,
        [none] (Metric, Counter));

    ECS_SYSTEM(world, UpdateCounterIncrementMemberInstance, EcsPreStore, 
        [out]  Value, 
        [in]   MemberInstance,
        [none] (Metric, CounterIncrement));

    ECS_SYSTEM(world, UpdateGaugeIdInstance, EcsPreStore, 
        [out]  Value, 
        [in]   IdInstance,
        [none] (Metric, Gauge));

    ECS_SYSTEM(world, UpdateCounterIdInstance, EcsPreStore, 
        [inout] Value, 
        [in]    IdInstance,
        [none]  (Metric, Counter));

    ECS_SYSTEM(world, UpdateGaugeOneOfInstance, EcsPreStore, 
        [none] (_, Value), 
        [in]   OneOfInstance,
        [none] (Metric, Gauge));

    ECS_SYSTEM(world, UpdateCounterOneOfInstance, EcsPreStore, 
        [none] (_, Value), 
        [in]   OneOfInstance,
        [none] (Metric, Counter));

    ECS_SYSTEM(world, UpdateCountIds, EcsPreStore, 
        [inout] CountIds, Value);

    ECS_SYSTEM(world, UpdateCountTargets, EcsPreStore, 
        [inout] CountTargets);
}

#endif

/**
 * @file meta/api.c
 * @brief API for creating entities with reflection data.
 */

/**
 * @file meta/meta.h
 * @brief Private functions for meta addon.
 */

#ifndef FLECS_META_PRIVATE_H
#define FLECS_META_PRIVATE_H


#ifdef FLECS_META

void ecs_meta_type_serialized_init(
    ecs_iter_t *it);

void ecs_meta_dtor_serialized(
    EcsMetaTypeSerialized *ptr);

ecs_meta_type_op_kind_t flecs_meta_primitive_to_op_kind(
    ecs_primitive_kind_t kind);

bool flecs_unit_validate(
    ecs_world_t *world,
    ecs_entity_t t,
    EcsUnit *data);

#endif
    
#endif


#ifdef FLECS_META

ecs_entity_t ecs_primitive_init(
    ecs_world_t *world,
    const ecs_primitive_desc_t *desc)
{
    ecs_entity_t t = desc->entity;
    if (!t) {
        t = ecs_new_low_id(world);
    }

    ecs_set(world, t, EcsPrimitive, { desc->kind });

    return t;
}

ecs_entity_t ecs_enum_init(
    ecs_world_t *world,
    const ecs_enum_desc_t *desc)
{
    ecs_entity_t t = desc->entity;
    if (!t) {
        t = ecs_new_low_id(world);
    }

    ecs_add(world, t, EcsEnum);

    ecs_entity_t old_scope = ecs_set_scope(world, t);

    int i;
    for (i = 0; i < ECS_MEMBER_DESC_CACHE_SIZE; i ++) {
        const ecs_enum_constant_t *m_desc = &desc->constants[i];
        if (!m_desc->name) {
            break;
        }

        ecs_entity_t c = ecs_entity(world, {
            .name = m_desc->name
        });

        if (!m_desc->value) {
            ecs_add_id(world, c, EcsConstant);
        } else {
            ecs_set_pair_object(world, c, EcsConstant, ecs_i32_t, 
                {m_desc->value});
        }
    }

    ecs_set_scope(world, old_scope);

    if (i == 0) {
        ecs_err("enum '%s' has no constants", ecs_get_name(world, t));
        ecs_delete(world, t);
        return 0;
    }

    return t;
}

ecs_entity_t ecs_bitmask_init(
    ecs_world_t *world,
    const ecs_bitmask_desc_t *desc)
{
    ecs_entity_t t = desc->entity;
    if (!t) {
        t = ecs_new_low_id(world);
    }

    ecs_add(world, t, EcsBitmask);

    ecs_entity_t old_scope = ecs_set_scope(world, t);

    int i;
    for (i = 0; i < ECS_MEMBER_DESC_CACHE_SIZE; i ++) {
        const ecs_bitmask_constant_t *m_desc = &desc->constants[i];
        if (!m_desc->name) {
            break;
        }

        ecs_entity_t c = ecs_entity(world, {
            .name = m_desc->name
        });

        if (!m_desc->value) {
            ecs_add_id(world, c, EcsConstant);
        } else {
            ecs_set_pair_object(world, c, EcsConstant, ecs_u32_t, 
                {m_desc->value});
        }
    }

    ecs_set_scope(world, old_scope);

    if (i == 0) {
        ecs_err("bitmask '%s' has no constants", ecs_get_name(world, t));
        ecs_delete(world, t);
        return 0;
    }

    return t;
}

ecs_entity_t ecs_array_init(
    ecs_world_t *world,
    const ecs_array_desc_t *desc)
{
    ecs_entity_t t = desc->entity;
    if (!t) {
        t = ecs_new_low_id(world);
    }

    ecs_set(world, t, EcsArray, {
        .type = desc->type,
        .count = desc->count
    });

    return t;
}

ecs_entity_t ecs_vector_init(
    ecs_world_t *world,
    const ecs_vector_desc_t *desc)
{
    ecs_entity_t t = desc->entity;
    if (!t) {
        t = ecs_new_low_id(world);
    }

    ecs_set(world, t, EcsVector, {
        .type = desc->type
    });

    return t;
}

ecs_entity_t ecs_struct_init(
    ecs_world_t *world,
    const ecs_struct_desc_t *desc)
{
    ecs_entity_t t = desc->entity;
    if (!t) {
        t = ecs_new_low_id(world);
    }

    ecs_entity_t old_scope = ecs_set_scope(world, t);

    int i;
    for (i = 0; i < ECS_MEMBER_DESC_CACHE_SIZE; i ++) {
        const ecs_member_t *m_desc = &desc->members[i];
        if (!m_desc->type) {
            break;
        }

        if (!m_desc->name) {
            ecs_err("member %d of struct '%s' does not have a name", i, 
                ecs_get_name(world, t));
            ecs_delete(world, t);
            return 0;
        }

        ecs_entity_t m = ecs_entity(world, {
            .name = m_desc->name
        });

        ecs_set(world, m, EcsMember, {
            .type = m_desc->type, 
            .count = m_desc->count,
            .offset = m_desc->offset,
            .unit = m_desc->unit
        });
    }

    ecs_set_scope(world, old_scope);

    if (i == 0) {
        ecs_err("struct '%s' has no members", ecs_get_name(world, t));
        ecs_delete(world, t);
        return 0;
    }

    if (!ecs_has(world, t, EcsStruct)) {
        /* Invalid members */
        ecs_delete(world, t);
        return 0;
    }

    return t;
}

ecs_entity_t ecs_opaque_init(
    ecs_world_t *world,
    const ecs_opaque_desc_t *desc)
{
    ecs_poly_assert(world, ecs_world_t);
    ecs_assert(desc != NULL, ECS_INVALID_PARAMETER, NULL);
    ecs_assert(desc->type.as_type != 0, ECS_INVALID_PARAMETER, NULL);

    ecs_entity_t t = desc->entity;
    if (!t) {
        t = ecs_new_low_id(world);
    }

    ecs_set_ptr(world, t, EcsOpaque, &desc->type);

    return t;
}

ecs_entity_t ecs_unit_init(
    ecs_world_t *world,
    const ecs_unit_desc_t *desc)
{
    ecs_entity_t t = desc->entity;
    if (!t) {
        t = ecs_new_low_id(world);
    }

    ecs_entity_t quantity = desc->quantity;
    if (quantity) {
        if (!ecs_has_id(world, quantity, EcsQuantity)) {
            ecs_err("entity '%s' for unit '%s' is not a quantity",
                ecs_get_name(world, quantity), ecs_get_name(world, t));
            goto error;
        }

        ecs_add_pair(world, t, EcsQuantity, desc->quantity);
    } else {
        ecs_remove_pair(world, t, EcsQuantity, EcsWildcard);
    }

    EcsUnit *value = ecs_get_mut(world, t, EcsUnit);
    value->base = desc->base;
    value->over = desc->over;
    value->translation = desc->translation;
    value->prefix = desc->prefix;
    ecs_os_strset(&value->symbol, desc->symbol);

    if (!flecs_unit_validate(world, t, value)) {
        goto error;
    }

    ecs_modified(world, t, EcsUnit);

    return t;
error:
    if (t) {
        ecs_delete(world, t);
    }
    return 0;
}

ecs_entity_t ecs_unit_prefix_init(
    ecs_world_t *world,
    const ecs_unit_prefix_desc_t *desc)
{
    ecs_entity_t t = desc->entity;
    if (!t) {
        t = ecs_new_low_id(world);
    }

    ecs_set(world, t, EcsUnitPrefix, {
        .symbol = (char*)desc->symbol,
        .translation = desc->translation
    });

    return t;
}

ecs_entity_t ecs_quantity_init(
    ecs_world_t *world,
    const ecs_entity_desc_t *desc)
{
    ecs_entity_t t = ecs_entity_init(world, desc);
    if (!t) {
        return 0;
    }

    ecs_add_id(world, t, EcsQuantity);

    return t;
}

#endif

/**
 * @file meta/serialized.c
 * @brief Serialize type into flat operations array to speed up deserialization.
 */


#ifdef FLECS_META

static
int flecs_meta_serialize_type(
    ecs_world_t *world,
    ecs_entity_t type,
    ecs_size_t offset,
    ecs_vec_t *ops);

ecs_meta_type_op_kind_t flecs_meta_primitive_to_op_kind(ecs_primitive_kind_t kind) {
    return EcsOpPrimitive + kind;
}

static
ecs_size_t flecs_meta_type_size(ecs_world_t *world, ecs_entity_t type) {
    const EcsComponent *comp = ecs_get(world, type, EcsComponent);
    ecs_assert(comp != NULL, ECS_INTERNAL_ERROR, NULL);
    return comp->size;
}

static
ecs_meta_type_op_t* flecs_meta_ops_add(ecs_vec_t *ops, ecs_meta_type_op_kind_t kind) {
    ecs_meta_type_op_t *op = ecs_vec_append_t(NULL, ops, ecs_meta_type_op_t);
    op->kind = kind;
    op->offset = 0;
    op->count = 1;
    op->op_count = 1;
    op->size = 0;
    op->name = NULL;
    op->members = NULL;
    op->type = 0;
    op->unit = 0;
    return op;
}

static
ecs_meta_type_op_t* flecs_meta_ops_get(ecs_vec_t *ops, int32_t index) {
    ecs_meta_type_op_t* op = ecs_vec_get_t(ops, ecs_meta_type_op_t, index);
    ecs_assert(op != NULL, ECS_INTERNAL_ERROR, NULL);
    return op;
}

static
int flecs_meta_serialize_primitive(
    ecs_world_t *world,
    ecs_entity_t type,
    ecs_size_t offset,
    ecs_vec_t *ops)
{
    const EcsPrimitive *ptr = ecs_get(world, type, EcsPrimitive);
    if (!ptr) {
        char *name = ecs_get_fullpath(world, type);
        ecs_err("entity '%s' is not a primitive type", name);
        ecs_os_free(name);
        return -1;
    }

    ecs_meta_type_op_t *op = flecs_meta_ops_add(ops, flecs_meta_primitive_to_op_kind(ptr->kind));
    op->offset = offset,
    op->type = type;
    op->size = flecs_meta_type_size(world, type);
    return 0;
}

static
int flecs_meta_serialize_enum(
    ecs_world_t *world,
    ecs_entity_t type,
    ecs_size_t offset,
    ecs_vec_t *ops)
{
    (void)world;
    
    ecs_meta_type_op_t *op = flecs_meta_ops_add(ops, EcsOpEnum);
    op->offset = offset,
    op->type = type;
    op->size = ECS_SIZEOF(ecs_i32_t);
    return 0;
}

static
int flecs_meta_serialize_bitmask(
    ecs_world_t *world,
    ecs_entity_t type,
    ecs_size_t offset,
    ecs_vec_t *ops)
{
    (void)world;
    
    ecs_meta_type_op_t *op = flecs_meta_ops_add(ops, EcsOpBitmask);
    op->offset = offset,
    op->type = type;
    op->size = ECS_SIZEOF(ecs_u32_t);
    return 0;
}

static
int flecs_meta_serialize_array(
    ecs_world_t *world,
    ecs_entity_t type,
    ecs_size_t offset,
    ecs_vec_t *ops)
{
    (void)world;

    ecs_meta_type_op_t *op = flecs_meta_ops_add(ops, EcsOpArray);
    op->offset = offset;
    op->type = type;
    op->size = flecs_meta_type_size(world, type);
    return 0;
}

static
int flecs_meta_serialize_array_component(
    ecs_world_t *world,
    ecs_entity_t type,
    ecs_vec_t *ops)
{
    const EcsArray *ptr = ecs_get(world, type, EcsArray);
    if (!ptr) {
        return -1; /* Should never happen, will trigger internal error */
    }

    flecs_meta_serialize_type(world, ptr->type, 0, ops);

    ecs_meta_type_op_t *first = ecs_vec_first(ops);
    first->count = ptr->count;
    return 0;
}

static
int flecs_meta_serialize_vector(
    ecs_world_t *world,
    ecs_entity_t type,
    ecs_size_t offset,
    ecs_vec_t *ops)
{
    (void)world;
    ecs_meta_type_op_t *op = flecs_meta_ops_add(ops, EcsOpVector);
    op->offset = offset;
    op->type = type;
    op->size = flecs_meta_type_size(world, type);
    return 0;
}

static
int flecs_meta_serialize_custom_type(
    ecs_world_t *world,
    ecs_entity_t type,
    ecs_size_t offset,
    ecs_vec_t *ops)
{
    (void)world;
    ecs_meta_type_op_t *op = flecs_meta_ops_add(ops, EcsOpOpaque);
    op->offset = offset;
    op->type = type;
    op->size = flecs_meta_type_size(world, type);
    return 0;
}

static
int flecs_meta_serialize_struct(
    ecs_world_t *world,
    ecs_entity_t type,
    ecs_size_t offset,
    ecs_vec_t *ops)
{
    const EcsStruct *ptr = ecs_get(world, type, EcsStruct);
    ecs_assert(ptr != NULL, ECS_INTERNAL_ERROR, NULL);

    int32_t cur, first = ecs_vec_count(ops);
    ecs_meta_type_op_t *op = flecs_meta_ops_add(ops, EcsOpPush);
    op->offset = offset;
    op->type = type;
    op->size = flecs_meta_type_size(world, type);

    ecs_member_t *members = ecs_vec_first(&ptr->members);
    int32_t i, count = ecs_vec_count(&ptr->members);

    ecs_hashmap_t *member_index = NULL;
    if (count) {        
        op->members = member_index = flecs_name_index_new(
            world, &world->allocator);
    }

    for (i = 0; i < count; i ++) {
        ecs_member_t *member = &members[i];

        cur = ecs_vec_count(ops);
        flecs_meta_serialize_type(world, member->type, offset + member->offset, ops);

        op = flecs_meta_ops_get(ops, cur);
        if (!op->type) {
            op->type = member->type;
        }

        if (op->count <= 1) {
            op->count = member->count;
        }

        const char *member_name = member->name;
        op->name = member_name;
        op->unit = member->unit;
        op->op_count = ecs_vec_count(ops) - cur;

        flecs_name_index_ensure(
            member_index, flecs_ito(uint64_t, cur - first - 1), 
                member_name, 0, 0);
    }

    flecs_meta_ops_add(ops, EcsOpPop);
    flecs_meta_ops_get(ops, first)->op_count = ecs_vec_count(ops) - first;
    return 0;
}

static
int flecs_meta_serialize_type(
    ecs_world_t *world,
    ecs_entity_t type,
    ecs_size_t offset,
    ecs_vec_t *ops)
{
    const EcsMetaType *ptr = ecs_get(world, type, EcsMetaType);
    if (!ptr) {
        char *path = ecs_get_fullpath(world, type);
        ecs_err("missing EcsMetaType for type %s'", path);
        ecs_os_free(path);
        return -1;
    }

    switch(ptr->kind) {
    case EcsPrimitiveType: return flecs_meta_serialize_primitive(world, type, offset, ops);
    case EcsEnumType: return flecs_meta_serialize_enum(world, type, offset, ops);
    case EcsBitmaskType: return flecs_meta_serialize_bitmask(world, type, offset, ops);
    case EcsStructType: return flecs_meta_serialize_struct(world, type, offset, ops);
    case EcsArrayType: return flecs_meta_serialize_array(world, type, offset, ops);
    case EcsVectorType: return flecs_meta_serialize_vector(world, type, offset, ops);
    case EcsOpaqueType: return flecs_meta_serialize_custom_type(world, type, offset, ops);
    }

    return 0;
}

static
int flecs_meta_serialize_component(
    ecs_world_t *world,
    ecs_entity_t type,
    ecs_vec_t *ops)
{
    const EcsMetaType *ptr = ecs_get(world, type, EcsMetaType);
    if (!ptr) {
        char *path = ecs_get_fullpath(world, type);
        ecs_err("missing EcsMetaType for type %s'", path);
        ecs_os_free(path);
        return -1;
    }

    switch(ptr->kind) {
    case EcsArrayType:
        return flecs_meta_serialize_array_component(world, type, ops);
        break;
    default:
        return flecs_meta_serialize_type(world, type, 0, ops);
        break;
    }

    return 0;
}

void ecs_meta_type_serialized_init(
    ecs_iter_t *it)
{
    ecs_world_t *world = it->world;

    int i, count = it->count;
    for (i = 0; i < count; i ++) {
        ecs_entity_t e = it->entities[i];
        ecs_vec_t ops;
        ecs_vec_init_t(NULL, &ops, ecs_meta_type_op_t, 0);
        flecs_meta_serialize_component(world, e, &ops);

        EcsMetaTypeSerialized *ptr = ecs_get_mut(
            world, e, EcsMetaTypeSerialized);
        if (ptr->ops.array) {
            ecs_meta_dtor_serialized(ptr);
        }

        ptr->ops = ops;
    }
}

#endif

/**
 * @file meta/meta.c
 * @brief Meta addon.
 */


#ifdef FLECS_META

/* ecs_string_t lifecycle */

static ECS_COPY(ecs_string_t, dst, src, {
    ecs_os_free(*(ecs_string_t*)dst);
    *(ecs_string_t*)dst = ecs_os_strdup(*(ecs_string_t*)src);
})

static ECS_MOVE(ecs_string_t, dst, src, {
    ecs_os_free(*(ecs_string_t*)dst);
    *(ecs_string_t*)dst = *(ecs_string_t*)src;
    *(ecs_string_t*)src = NULL;
})

static ECS_DTOR(ecs_string_t, ptr, { 
    ecs_os_free(*(ecs_string_t*)ptr);
    *(ecs_string_t*)ptr = NULL;
})


/* EcsMetaTypeSerialized lifecycle */

void ecs_meta_dtor_serialized(
    EcsMetaTypeSerialized *ptr) 
{
    int32_t i, count = ecs_vec_count(&ptr->ops);
    ecs_meta_type_op_t *ops = ecs_vec_first(&ptr->ops);
    
    for (i = 0; i < count; i ++) {
        ecs_meta_type_op_t *op = &ops[i];
        if (op->members) {
            flecs_name_index_free(op->members);
        }
    }

    ecs_vec_fini_t(NULL, &ptr->ops, ecs_meta_type_op_t);
}

static ECS_COPY(EcsMetaTypeSerialized, dst, src, {
    ecs_meta_dtor_serialized(dst);

    dst->ops = ecs_vec_copy_t(NULL, &src->ops, ecs_meta_type_op_t);

    int32_t o, count = ecs_vec_count(&dst->ops);
    ecs_meta_type_op_t *ops = ecs_vec_first_t(&dst->ops, ecs_meta_type_op_t);
    
    for (o = 0; o < count; o ++) {
        ecs_meta_type_op_t *op = &ops[o];
        if (op->members) {
            op->members = flecs_name_index_copy(op->members);
        }
    }
})

static ECS_MOVE(EcsMetaTypeSerialized, dst, src, {
    ecs_meta_dtor_serialized(dst);
    dst->ops = src->ops;
    src->ops = (ecs_vec_t){0};
})

static ECS_DTOR(EcsMetaTypeSerialized, ptr, { 
    ecs_meta_dtor_serialized(ptr);
})


/* EcsStruct lifecycle */

static void flecs_struct_dtor(
    EcsStruct *ptr) 
{
    ecs_member_t *members = ecs_vec_first_t(&ptr->members, ecs_member_t);
    int32_t i, count = ecs_vec_count(&ptr->members);
    for (i = 0; i < count; i ++) {
        ecs_os_free((char*)members[i].name);
    }
    ecs_vec_fini_t(NULL, &ptr->members, ecs_member_t);
}

static ECS_COPY(EcsStruct, dst, src, {
    flecs_struct_dtor(dst);

    dst->members = ecs_vec_copy_t(NULL, &src->members, ecs_member_t);

    ecs_member_t *members = ecs_vec_first_t(&dst->members, ecs_member_t);
    int32_t m, count = ecs_vec_count(&dst->members);

    for (m = 0; m < count; m ++) {
        members[m].name = ecs_os_strdup(members[m].name);
    }
})

static ECS_MOVE(EcsStruct, dst, src, {
    flecs_struct_dtor(dst);
    dst->members = src->members;
    src->members = (ecs_vec_t){0};
})

static ECS_DTOR(EcsStruct, ptr, { flecs_struct_dtor(ptr); })


/* EcsEnum lifecycle */

static void flecs_constants_dtor(
    ecs_map_t *constants) 
{
    ecs_map_iter_t it = ecs_map_iter(constants);
    while (ecs_map_next(&it)) {
        ecs_enum_constant_t *c = ecs_map_ptr(&it);
        ecs_os_free((char*)c->name);
        ecs_os_free(c);
    }
    ecs_map_fini(constants);
}

static void flecs_constants_copy(
    ecs_map_t *dst,
    ecs_map_t *src)
{
    ecs_map_copy(dst, src);

    ecs_map_iter_t it = ecs_map_iter(dst);
    while (ecs_map_next(&it)) {
        ecs_enum_constant_t **r = ecs_map_ref(&it, ecs_enum_constant_t);
        ecs_enum_constant_t *src_c = r[0];
        ecs_enum_constant_t *dst_c = ecs_os_calloc_t(ecs_enum_constant_t);
        *dst_c = *src_c;
        dst_c->name = ecs_os_strdup(dst_c->name);
        r[0] = dst_c;
    }
}

static ECS_COPY(EcsEnum, dst, src, {
    flecs_constants_dtor(&dst->constants);
    flecs_constants_copy(&dst->constants, &src->constants);
})

static ECS_MOVE(EcsEnum, dst, src, {
    flecs_constants_dtor(&dst->constants);
    dst->constants = src->constants;
    ecs_os_zeromem(&src->constants);
})

static ECS_DTOR(EcsEnum, ptr, { flecs_constants_dtor(&ptr->constants); })


/* EcsBitmask lifecycle */

static ECS_COPY(EcsBitmask, dst, src, {
    /* bitmask constant & enum constant have the same layout */
    flecs_constants_dtor(&dst->constants);
    flecs_constants_copy(&dst->constants, &src->constants);
})

static ECS_MOVE(EcsBitmask, dst, src, {
    flecs_constants_dtor(&dst->constants);
    dst->constants = src->constants;
    ecs_os_zeromem(&src->constants);
})

static ECS_DTOR(EcsBitmask, ptr, { flecs_constants_dtor(&ptr->constants); })


/* EcsUnit lifecycle */

static void dtor_unit(
    EcsUnit *ptr) 
{
    ecs_os_free(ptr->symbol);
}

static ECS_COPY(EcsUnit, dst, src, {
    dtor_unit(dst);
    dst->symbol = ecs_os_strdup(src->symbol);
    dst->base = src->base;
    dst->over = src->over;
    dst->prefix = src->prefix;
    dst->translation = src->translation;
})

static ECS_MOVE(EcsUnit, dst, src, {
    dtor_unit(dst);
    dst->symbol = src->symbol;
    dst->base = src->base;
    dst->over = src->over;
    dst->prefix = src->prefix;
    dst->translation = src->translation;

    src->symbol = NULL;
    src->base = 0;
    src->over = 0;
    src->prefix = 0;
    src->translation = (ecs_unit_translation_t){0};
})

static ECS_DTOR(EcsUnit, ptr, { dtor_unit(ptr); })


/* EcsUnitPrefix lifecycle */

static void dtor_unit_prefix(
    EcsUnitPrefix *ptr) 
{
    ecs_os_free(ptr->symbol);
}

static ECS_COPY(EcsUnitPrefix, dst, src, {
    dtor_unit_prefix(dst);
    dst->symbol = ecs_os_strdup(src->symbol);
    dst->translation = src->translation;
})

static ECS_MOVE(EcsUnitPrefix, dst, src, {
    dtor_unit_prefix(dst);
    dst->symbol = src->symbol;
    dst->translation = src->translation;

    src->symbol = NULL;
    src->translation = (ecs_unit_translation_t){0};
})

static ECS_DTOR(EcsUnitPrefix, ptr, { dtor_unit_prefix(ptr); })

/* Type initialization */

static
const char* flecs_type_kind_str(
    ecs_type_kind_t kind)
{
    switch(kind) {
    case EcsPrimitiveType: return "Primitive";
    case EcsBitmaskType: return "Bitmask";
    case EcsEnumType: return "Enum";
    case EcsStructType: return "Struct";
    case EcsArrayType: return "Array";
    case EcsVectorType: return "Vector";
    case EcsOpaqueType: return "Opaque";
    default: return "unknown";
    }
}

static
int flecs_init_type(
    ecs_world_t *world,
    ecs_entity_t type,
    ecs_type_kind_t kind,
    ecs_size_t size,
    ecs_size_t alignment)
{
    ecs_assert(world != NULL, ECS_INTERNAL_ERROR, NULL);
    ecs_assert(type != 0, ECS_INTERNAL_ERROR, NULL);

    EcsMetaType *meta_type = ecs_get_mut(world, type, EcsMetaType);
    if (meta_type->kind == 0) {
        meta_type->existing = ecs_has(world, type, EcsComponent);

        /* Ensure that component has a default constructor, to prevent crashing
         * serializers on uninitialized values. */
        ecs_type_info_t *ti = flecs_type_info_ensure(world, type);
        if (!ti->hooks.ctor) {
            ti->hooks.ctor = ecs_default_ctor;
        }
    } else {
        if (meta_type->kind != kind) {
            ecs_err("type '%s' reregistered as '%s' (was '%s')", 
                ecs_get_name(world, type), 
                flecs_type_kind_str(kind),
                flecs_type_kind_str(meta_type->kind));
            return -1;
        }
    }

    if (!meta_type->existing) {
        EcsComponent *comp = ecs_get_mut(world, type, EcsComponent);
        comp->size = size;
        comp->alignment = alignment;
        ecs_modified(world, type, EcsComponent);
    } else {
        const EcsComponent *comp = ecs_get(world, type, EcsComponent);
        if (comp->size < size) {
            ecs_err("computed size (%d) for '%s' is larger than actual type (%d)", 
                size, ecs_get_name(world, type), comp->size);
            return -1;
        }
        if (comp->alignment < alignment) {
            ecs_err("computed alignment (%d) for '%s' is larger than actual type (%d)", 
                alignment, ecs_get_name(world, type), comp->alignment);
            return -1;
        }
        if (comp->size == size && comp->alignment != alignment) {
            ecs_err("computed size for '%s' matches with actual type but "
                "alignment is different (%d vs. %d)", ecs_get_name(world, type),
                    alignment, comp->alignment);
            return -1;
        }
        
        meta_type->partial = comp->size != size;
    }

    meta_type->kind = kind;
    meta_type->size = size;
    meta_type->alignment = alignment;
    ecs_modified(world, type, EcsMetaType);

    return 0;
}

#define init_type_t(world, type, kind, T) \
    flecs_init_type(world, type, kind, ECS_SIZEOF(T), ECS_ALIGNOF(T))

static
void flecs_set_struct_member(
    ecs_member_t *member,
    ecs_entity_t entity,
    const char *name,
    ecs_entity_t type,
    int32_t count,
    int32_t offset,
    ecs_entity_t unit)
{
    member->member = entity;
    member->type = type;
    member->count = count;
    member->unit = unit;
    member->offset = offset;

    if (!count) {
        member->count = 1;
    }

    ecs_os_strset((char**)&member->name, name);
}

static
int flecs_add_member_to_struct(
    ecs_world_t *world,
    ecs_entity_t type,
    ecs_entity_t member,
    EcsMember *m)
{
    ecs_assert(world != NULL, ECS_INTERNAL_ERROR, NULL);
    ecs_assert(type != 0, ECS_INTERNAL_ERROR, NULL);
    ecs_assert(member != 0, ECS_INTERNAL_ERROR, NULL);
    ecs_assert(m != NULL, ECS_INTERNAL_ERROR, NULL);

    const char *name = ecs_get_name(world, member);
    if (!name) {
        char *path = ecs_get_fullpath(world, type);
        ecs_err("member for struct '%s' does not have a name", path);
        ecs_os_free(path);
        return -1;
    }

    if (!m->type) {
        char *path = ecs_get_fullpath(world, member);
        ecs_err("member '%s' does not have a type", path);
        ecs_os_free(path);
        return -1;
    }

    if (ecs_get_typeid(world, m->type) == 0) {
        char *path = ecs_get_fullpath(world, member);
        char *ent_path = ecs_get_fullpath(world, m->type);
        ecs_err("member '%s.type' is '%s' which is not a type", path, ent_path);
        ecs_os_free(path);
        ecs_os_free(ent_path);
        return -1;
    }

    ecs_entity_t unit = m->unit;

    if (unit) {
        if (!ecs_has(world, unit, EcsUnit)) {
            ecs_err("entity '%s' for member '%s' is not a unit",
                ecs_get_name(world, unit), name);
            return -1;
        }

        if (ecs_has(world, m->type, EcsUnit) && m->type != unit) {
            ecs_err("unit mismatch for type '%s' and unit '%s' for member '%s'",
                ecs_get_name(world, m->type), ecs_get_name(world, unit), name);
            return -1;
        }
    } else {
        if (ecs_has(world, m->type, EcsUnit)) {
            unit = m->type;
            m->unit = unit;
        }
    }

    EcsStruct *s = ecs_get_mut(world, type, EcsStruct);
    ecs_assert(s != NULL, ECS_INTERNAL_ERROR, NULL);

    /* First check if member is already added to struct */
    ecs_member_t *members = ecs_vec_first_t(&s->members, ecs_member_t);
    int32_t i, count = ecs_vec_count(&s->members);
    for (i = 0; i < count; i ++) {
        if (members[i].member == member) {
            flecs_set_struct_member(
                &members[i], member, name, m->type, m->count, m->offset, unit);
            break;
        }
    }

    /* If member wasn't added yet, add a new element to vector */
    if (i == count) {
        ecs_vec_init_if_t(&s->members, ecs_member_t);
        ecs_member_t *elem = ecs_vec_append_t(NULL, &s->members, ecs_member_t);
        elem->name = NULL;
        flecs_set_struct_member(elem, member, name, m->type, 
            m->count, m->offset, unit);

        /* Reobtain members array in case it was reallocated */
        members = ecs_vec_first_t(&s->members, ecs_member_t);
        count ++;
    }

    bool explicit_offset = false;
    if (m->offset) {
        explicit_offset = true;
    }

    /* Compute member offsets and size & alignment of struct */
    ecs_size_t size = 0;
    ecs_size_t alignment = 0;

    if (!explicit_offset) {
        for (i = 0; i < count; i ++) {
            ecs_member_t *elem = &members[i];

            ecs_assert(elem->name != NULL, ECS_INTERNAL_ERROR, NULL);
            ecs_assert(elem->type != 0, ECS_INTERNAL_ERROR, NULL);

            /* Get component of member type to get its size & alignment */
            const EcsComponent *mbr_comp = ecs_get(world, elem->type, EcsComponent);
            if (!mbr_comp) {
                char *path = ecs_get_fullpath(world, member);
                ecs_err("member '%s' is not a type", path);
                ecs_os_free(path);
                return -1;
            }

            ecs_size_t member_size = mbr_comp->size;
            ecs_size_t member_alignment = mbr_comp->alignment;

            if (!member_size || !member_alignment) {
                char *path = ecs_get_fullpath(world, member);
                ecs_err("member '%s' has 0 size/alignment");
                ecs_os_free(path);
                return -1;
            }

            member_size *= elem->count;
            size = ECS_ALIGN(size, member_alignment);
            elem->size = member_size;
            elem->offset = size;

            /* Synchronize offset with Member component */
            if (elem->member == member) {
                m->offset = elem->offset;
            } else {
                EcsMember *other = ecs_get_mut(world, elem->member, EcsMember);
                other->offset = elem->offset;
            }

            size += member_size;

            if (member_alignment > alignment) {
                alignment = member_alignment;
            }
        }
    } else {
        /* If members have explicit offsets, we can't rely on computed 
         * size/alignment values. Grab size of just added member instead. It
         * doesn't matter if the size doesn't correspond to the actual struct
         * size. The flecs_init_type function compares computed size with actual
         * (component) size to determine if the type is partial. */
        const EcsComponent *cptr = ecs_get(world, m->type, EcsComponent);
        ecs_assert(cptr != NULL, ECS_INTERNAL_ERROR, NULL);
        size = cptr->size;
        alignment = cptr->alignment;
    }

    if (size == 0) {
        ecs_err("struct '%s' has 0 size", ecs_get_name(world, type));
        return -1;
    }

    if (alignment == 0) {
        ecs_err("struct '%s' has 0 alignment", ecs_get_name(world, type));
        return -1;
    }

    /* Align struct size to struct alignment */
    size = ECS_ALIGN(size, alignment);

    ecs_modified(world, type, EcsStruct);

    /* Do this last as it triggers the update of EcsMetaTypeSerialized */
    if (flecs_init_type(world, type, EcsStructType, size, alignment)) {
        return -1;
    }

    /* If current struct is also a member, assign to itself */
    if (ecs_has(world, type, EcsMember)) {
        EcsMember *type_mbr = ecs_get_mut(world, type, EcsMember);
        ecs_assert(type_mbr != NULL, ECS_INTERNAL_ERROR, NULL);

        type_mbr->type = type;
        type_mbr->count = 1;

        ecs_modified(world, type, EcsMember);
    }

    return 0;
}

static
int flecs_add_constant_to_enum(
    ecs_world_t *world, 
    ecs_entity_t type, 
    ecs_entity_t e,
    ecs_id_t constant_id)
{
    EcsEnum *ptr = ecs_get_mut(world, type, EcsEnum);

    /* Remove constant from map if it was already added */
    ecs_map_iter_t it = ecs_map_iter(&ptr->constants);
    while (ecs_map_next(&it)) {
        ecs_enum_constant_t *c = ecs_map_ptr(&it);
        if (c->constant == e) {
            ecs_os_free((char*)c->name);
            ecs_map_remove_free(&ptr->constants, ecs_map_key(&it));
        }
    }

    /* Check if constant sets explicit value */
    int32_t value = 0;
    bool value_set = false;
    if (ecs_id_is_pair(constant_id)) {
        if (ecs_pair_second(world, constant_id) != ecs_id(ecs_i32_t)) {
            char *path = ecs_get_fullpath(world, e);
            ecs_err("expected i32 type for enum constant '%s'", path);
            ecs_os_free(path);
            return -1;
        }

        const int32_t *value_ptr = ecs_get_pair_object(
            world, e, EcsConstant, ecs_i32_t);
        ecs_assert(value_ptr != NULL, ECS_INTERNAL_ERROR, NULL);
        value = *value_ptr;
        value_set = true;
    }

    /* Make sure constant value doesn't conflict if set / find the next value */
    it = ecs_map_iter(&ptr->constants);
    while (ecs_map_next(&it)) {
        ecs_enum_constant_t *c = ecs_map_ptr(&it);
        if (value_set) {
            if (c->value == value) {
                char *path = ecs_get_fullpath(world, e);
                ecs_err("conflicting constant value %d for '%s' (other is '%s')",
                    value, path, c->name);
                ecs_os_free(path);
                return -1;
            }
        } else {
            if (c->value >= value) {
                value = c->value + 1;
            }
        }
    }

    ecs_map_init_if(&ptr->constants, &world->allocator);
    ecs_enum_constant_t *c = ecs_map_insert_alloc_t(&ptr->constants, 
        ecs_enum_constant_t, (ecs_map_key_t)value);
    c->name = ecs_os_strdup(ecs_get_name(world, e));
    c->value = value;
    c->constant = e;

    ecs_i32_t *cptr = ecs_get_mut_pair_object(
        world, e, EcsConstant, ecs_i32_t);
    ecs_assert(cptr != NULL, ECS_INTERNAL_ERROR, NULL);
    cptr[0] = value;

    cptr = ecs_get_mut_id(world, e, type);
    cptr[0] = value;

    return 0;
}

static
int flecs_add_constant_to_bitmask(
    ecs_world_t *world, 
    ecs_entity_t type, 
    ecs_entity_t e,
    ecs_id_t constant_id)
{
    EcsBitmask *ptr = ecs_get_mut(world, type, EcsBitmask);
    
    /* Remove constant from map if it was already added */
    ecs_map_iter_t it = ecs_map_iter(&ptr->constants);
    while (ecs_map_next(&it)) {
        ecs_bitmask_constant_t *c = ecs_map_ptr(&it);
        if (c->constant == e) {
            ecs_os_free((char*)c->name);
            ecs_map_remove_free(&ptr->constants, ecs_map_key(&it));
        }
    }

    /* Check if constant sets explicit value */
    uint32_t value = 1;
    if (ecs_id_is_pair(constant_id)) {
        if (ecs_pair_second(world, constant_id) != ecs_id(ecs_u32_t)) {
            char *path = ecs_get_fullpath(world, e);
            ecs_err("expected u32 type for bitmask constant '%s'", path);
            ecs_os_free(path);
            return -1;
        }

        const uint32_t *value_ptr = ecs_get_pair_object(
            world, e, EcsConstant, ecs_u32_t);
        ecs_assert(value_ptr != NULL, ECS_INTERNAL_ERROR, NULL);
        value = *value_ptr;
    } else {
        value = 1u << (ecs_u32_t)ecs_map_count(&ptr->constants);
    }

    /* Make sure constant value doesn't conflict */
    it = ecs_map_iter(&ptr->constants);
    while  (ecs_map_next(&it)) {
        ecs_bitmask_constant_t *c = ecs_map_ptr(&it);
        if (c->value == value) {
            char *path = ecs_get_fullpath(world, e);
            ecs_err("conflicting constant value for '%s' (other is '%s')",
                path, c->name);
            ecs_os_free(path);
            return -1;
        }
    }

    ecs_map_init_if(&ptr->constants, &world->allocator);

    ecs_bitmask_constant_t *c = ecs_map_insert_alloc_t(&ptr->constants, 
        ecs_bitmask_constant_t, value);
    c->name = ecs_os_strdup(ecs_get_name(world, e));
    c->value = value;
    c->constant = e;

    ecs_u32_t *cptr = ecs_get_mut_pair_object(
        world, e, EcsConstant, ecs_u32_t);
    ecs_assert(cptr != NULL, ECS_INTERNAL_ERROR, NULL);
    cptr[0] = value;

    cptr = ecs_get_mut_id(world, e, type);
    cptr[0] = value;

    return 0;
}

static
void flecs_set_primitive(ecs_iter_t *it) {
    ecs_world_t *world = it->world;
    EcsPrimitive *type = ecs_field(it, EcsPrimitive, 1);

    int i, count = it->count;
    for (i = 0; i < count; i ++) {
        ecs_entity_t e = it->entities[i];
        switch(type->kind) {
        case EcsBool:
            init_type_t(world, e, EcsPrimitiveType, bool);
            break;
        case EcsChar:
            init_type_t(world, e, EcsPrimitiveType, char);
            break;
        case EcsByte:
            init_type_t(world, e, EcsPrimitiveType, bool);
            break;
        case EcsU8:
            init_type_t(world, e, EcsPrimitiveType, uint8_t);
            break;
        case EcsU16:
            init_type_t(world, e, EcsPrimitiveType, uint16_t);
            break;
        case EcsU32:
            init_type_t(world, e, EcsPrimitiveType, uint32_t);
            break;
        case EcsU64:
            init_type_t(world, e, EcsPrimitiveType, uint64_t);
            break;
        case EcsI8:
            init_type_t(world, e, EcsPrimitiveType, int8_t);
            break;
        case EcsI16:
            init_type_t(world, e, EcsPrimitiveType, int16_t);
            break;
        case EcsI32:
            init_type_t(world, e, EcsPrimitiveType, int32_t);
            break;
        case EcsI64:
            init_type_t(world, e, EcsPrimitiveType, int64_t);
            break;
        case EcsF32:
            init_type_t(world, e, EcsPrimitiveType, float);
            break;
        case EcsF64:
            init_type_t(world, e, EcsPrimitiveType, double);
            break;
        case EcsUPtr:
            init_type_t(world, e, EcsPrimitiveType, uintptr_t);
            break;
        case EcsIPtr:
            init_type_t(world, e, EcsPrimitiveType, intptr_t);
            break;
        case EcsString:
            init_type_t(world, e, EcsPrimitiveType, char*);
            break;
        case EcsEntity:
            init_type_t(world, e, EcsPrimitiveType, ecs_entity_t);
            break;
        }
    }
}

static
void flecs_set_member(ecs_iter_t *it) {
    ecs_world_t *world = it->world;
    EcsMember *member = ecs_field(it, EcsMember, 1);

    int i, count = it->count;
    for (i = 0; i < count; i ++) {
        ecs_entity_t e = it->entities[i];
        ecs_entity_t parent = ecs_get_target(world, e, EcsChildOf, 0);
        if (!parent) {
            ecs_err("missing parent for member '%s'", ecs_get_name(world, e));
            continue;
        }

        flecs_add_member_to_struct(world, parent, e, &member[i]);
    }
}

static
void flecs_add_enum(ecs_iter_t *it) {
    ecs_world_t *world = it->world;

    int i, count = it->count;
    for (i = 0; i < count; i ++) {
        ecs_entity_t e = it->entities[i];

        if (init_type_t(world, e, EcsEnumType, ecs_i32_t)) {
            continue;
        }

        ecs_add_id(world, e, EcsExclusive);
        ecs_add_id(world, e, EcsOneOf);
        ecs_add_id(world, e, EcsTag);
    }
}

static
void flecs_add_bitmask(ecs_iter_t *it) {
    ecs_world_t *world = it->world;

    int i, count = it->count;
    for (i = 0; i < count; i ++) {
        ecs_entity_t e = it->entities[i];

        if (init_type_t(world, e, EcsBitmaskType, ecs_u32_t)) {
            continue;
        }
    }
}

static
void flecs_add_constant(ecs_iter_t *it) {
    ecs_world_t *world = it->world;

    int i, count = it->count;
    for (i = 0; i < count; i ++) {
        ecs_entity_t e = it->entities[i];
        ecs_entity_t parent = ecs_get_target(world, e, EcsChildOf, 0);
        if (!parent) {
            ecs_err("missing parent for constant '%s'", ecs_get_name(world, e));
            continue;
        }

        if (ecs_has(world, parent, EcsEnum)) {
            flecs_add_constant_to_enum(world, parent, e, it->event_id);
        } else if (ecs_has(world, parent, EcsBitmask)) {
            flecs_add_constant_to_bitmask(world, parent, e, it->event_id);
        }
    }
}

static
void flecs_set_array(ecs_iter_t *it) {
    ecs_world_t *world = it->world;
    EcsArray *array = ecs_field(it, EcsArray, 1);

    int i, count = it->count;
    for (i = 0; i < count; i ++) {
        ecs_entity_t e = it->entities[i];
        ecs_entity_t elem_type = array[i].type;
        int32_t elem_count = array[i].count;

        if (!elem_type) {
            ecs_err("array '%s' has no element type", ecs_get_name(world, e));
            continue;
        }

        if (!elem_count) {
            ecs_err("array '%s' has size 0", ecs_get_name(world, e));
            continue;
        }

        const EcsComponent *elem_ptr = ecs_get(world, elem_type, EcsComponent);
        if (flecs_init_type(world, e, EcsArrayType, 
            elem_ptr->size * elem_count, elem_ptr->alignment)) 
        {
            continue;
        }
    }
}

static
void flecs_set_vector(ecs_iter_t *it) {
    ecs_world_t *world = it->world;
    EcsVector *array = ecs_field(it, EcsVector, 1);

    int i, count = it->count;
    for (i = 0; i < count; i ++) {
        ecs_entity_t e = it->entities[i];
        ecs_entity_t elem_type = array[i].type;

        if (!elem_type) {
            ecs_err("vector '%s' has no element type", ecs_get_name(world, e));
            continue;
        }

        if (init_type_t(world, e, EcsVectorType, ecs_vec_t)) {
            continue;
        }
    }
}

static
void flecs_set_custom_type(ecs_iter_t *it) {
    ecs_world_t *world = it->world;
    EcsOpaque *serialize = ecs_field(it, EcsOpaque, 1);

    int i, count = it->count;
    for (i = 0; i < count; i ++) {
        ecs_entity_t e = it->entities[i];
        ecs_entity_t elem_type = serialize[i].as_type;

        if (!elem_type) {
            ecs_err("custom type '%s' has no mapping type", ecs_get_name(world, e));
            continue;
        }

        const EcsComponent *comp = ecs_get(world, e, EcsComponent);
        if (!comp || !comp->size || !comp->alignment) {
            ecs_err("custom type '%s' has no size/alignment, register as component first",
                ecs_get_name(world, e));
            continue;
        }

        if (flecs_init_type(world, e, EcsOpaqueType, comp->size, comp->alignment)) {
            continue;
        }
    }
}

bool flecs_unit_validate(
    ecs_world_t *world,
    ecs_entity_t t,
    EcsUnit *data)
{
    char *derived_symbol = NULL;
    const char *symbol = data->symbol;

    ecs_entity_t base = data->base;
    ecs_entity_t over = data->over;
    ecs_entity_t prefix = data->prefix;
    ecs_unit_translation_t translation = data->translation;

    if (base) {
        if (!ecs_has(world, base, EcsUnit)) {
            ecs_err("entity '%s' for unit '%s' used as base is not a unit",
                ecs_get_name(world, base), ecs_get_name(world, t));
            goto error;
        }
    }

    if (over) {
        if (!base) {
            ecs_err("invalid unit '%s': cannot specify over without base",
                ecs_get_name(world, t));
            goto error;
        }
        if (!ecs_has(world, over, EcsUnit)) {
            ecs_err("entity '%s' for unit '%s' used as over is not a unit",
                ecs_get_name(world, over), ecs_get_name(world, t));
            goto error;
        }
    }

    if (prefix) {
        if (!base) {
            ecs_err("invalid unit '%s': cannot specify prefix without base",
                ecs_get_name(world, t));
            goto error;
        }
        const EcsUnitPrefix *prefix_ptr = ecs_get(world, prefix, EcsUnitPrefix);
        if (!prefix_ptr) {
            ecs_err("entity '%s' for unit '%s' used as prefix is not a prefix",
                ecs_get_name(world, over), ecs_get_name(world, t));
            goto error;
        }

        if (translation.factor || translation.power) {
            if (prefix_ptr->translation.factor != translation.factor ||
                prefix_ptr->translation.power != translation.power)
            {
                ecs_err(
                    "factor for unit '%s' is inconsistent with prefix '%s'",
                    ecs_get_name(world, t), ecs_get_name(world, prefix));
                goto error;
            }
        } else {
            translation = prefix_ptr->translation;
        }
    }

    if (base) {
        bool must_match = false; /* Must base symbol match symbol? */
        ecs_strbuf_t sbuf = ECS_STRBUF_INIT;
        if (prefix) {
            const EcsUnitPrefix *ptr = ecs_get(world, prefix, EcsUnitPrefix);
            ecs_assert(ptr != NULL, ECS_INTERNAL_ERROR, NULL);
            if (ptr->symbol) {
                ecs_strbuf_appendstr(&sbuf, ptr->symbol);
                must_match = true;
            }
        }

        const EcsUnit *uptr = ecs_get(world, base, EcsUnit);
        ecs_assert(uptr != NULL, ECS_INTERNAL_ERROR, NULL);
        if (uptr->symbol) {
            ecs_strbuf_appendstr(&sbuf, uptr->symbol);
        }

        if (over) {
            uptr = ecs_get(world, over, EcsUnit);
            ecs_assert(uptr != NULL, ECS_INTERNAL_ERROR, NULL);
            if (uptr->symbol) {
                ecs_strbuf_appendch(&sbuf, '/');
                ecs_strbuf_appendstr(&sbuf, uptr->symbol);
                must_match = true;
            }
        }

        derived_symbol = ecs_strbuf_get(&sbuf);
        if (derived_symbol && !ecs_os_strlen(derived_symbol)) {
            ecs_os_free(derived_symbol);
            derived_symbol = NULL;
        }

        if (derived_symbol && symbol && ecs_os_strcmp(symbol, derived_symbol)) {
            if (must_match) {
                ecs_err("symbol '%s' for unit '%s' does not match base"
                    " symbol '%s'", symbol, 
                        ecs_get_name(world, t), derived_symbol);
                goto error;
            }
        }
        if (!symbol && derived_symbol && (prefix || over)) {
            ecs_os_free(data->symbol);
            data->symbol = derived_symbol;
        } else {
            ecs_os_free(derived_symbol);
        }
    }

    data->base = base;
    data->over = over;
    data->prefix = prefix;
    data->translation = translation;

    return true;
error:
    ecs_os_free(derived_symbol);
    return false;
}

static
void flecs_set_unit(ecs_iter_t *it) {
    EcsUnit *u = ecs_field(it, EcsUnit, 1);

    ecs_world_t *world = it->world;

    int i, count = it->count;
    for (i = 0; i < count; i ++) {
        ecs_entity_t e = it->entities[i];
        flecs_unit_validate(world, e, &u[i]);
    }
}

static
void flecs_unit_quantity_monitor(ecs_iter_t *it) {
    ecs_world_t *world = it->world;

    int i, count = it->count;
    if (it->event == EcsOnAdd) {
        for (i = 0; i < count; i ++) {
            ecs_entity_t e = it->entities[i];
            ecs_add_pair(world, e, EcsQuantity, e);
        }
    } else {
        for (i = 0; i < count; i ++) {
            ecs_entity_t e = it->entities[i];
            ecs_remove_pair(world, e, EcsQuantity, e);
        }
    }
}

static
void ecs_meta_type_init_default_ctor(ecs_iter_t *it) {
    ecs_world_t *world = it->world;
    EcsMetaType *type = ecs_field(it, EcsMetaType, 1);

    int i;
    for (i = 0; i < it->count; i ++) {
        /* If a component is defined from reflection data, configure it with the
         * default constructor. This ensures that a new component value does not
         * contain uninitialized memory, which could cause serializers to crash
         * when for example inspecting string fields. */
        if (!type->existing) {
            ecs_entity_t e = it->entities[i];
            const ecs_type_info_t *ti = ecs_get_type_info(world, e);
            if (!ti || !ti->hooks.ctor) {
                ecs_set_hooks_id(world, e, 
                    &(ecs_type_hooks_t){ 
                        .ctor = ecs_default_ctor
                    });
            }
        }
    }
}

static
void flecs_member_on_set(ecs_iter_t *it) {
    EcsMember *mbr = it->ptrs[0];
    if (!mbr->count) {
        mbr->count = 1;
    }
}

void FlecsMetaImport(
    ecs_world_t *world)
{
    ECS_MODULE(world, FlecsMeta);

    ecs_set_name_prefix(world, "Ecs");

    flecs_bootstrap_component(world, EcsMetaType);
    flecs_bootstrap_component(world, EcsMetaTypeSerialized);
    flecs_bootstrap_component(world, EcsPrimitive);
    flecs_bootstrap_component(world, EcsEnum);
    flecs_bootstrap_component(world, EcsBitmask);
    flecs_bootstrap_component(world, EcsMember);
    flecs_bootstrap_component(world, EcsStruct);
    flecs_bootstrap_component(world, EcsArray);
    flecs_bootstrap_component(world, EcsVector);
    flecs_bootstrap_component(world, EcsOpaque);
    flecs_bootstrap_component(world, EcsUnit);
    flecs_bootstrap_component(world, EcsUnitPrefix);

    flecs_bootstrap_tag(world, EcsConstant);
    flecs_bootstrap_tag(world, EcsQuantity);

    ecs_set_hooks(world, EcsMetaType, { .ctor = ecs_default_ctor });

    ecs_set_hooks(world, EcsMetaTypeSerialized, { 
        .ctor = ecs_default_ctor,
        .move = ecs_move(EcsMetaTypeSerialized),
        .copy = ecs_copy(EcsMetaTypeSerialized),
        .dtor = ecs_dtor(EcsMetaTypeSerialized)
    });

    ecs_set_hooks(world, EcsStruct, { 
        .ctor = ecs_default_ctor,
        .move = ecs_move(EcsStruct),
        .copy = ecs_copy(EcsStruct),
        .dtor = ecs_dtor(EcsStruct)
    });

    ecs_set_hooks(world, EcsMember, { 
        .ctor = ecs_default_ctor,
        .on_set = flecs_member_on_set
    });

    ecs_set_hooks(world, EcsEnum, { 
        .ctor = ecs_default_ctor,
        .move = ecs_move(EcsEnum),
        .copy = ecs_copy(EcsEnum),
        .dtor = ecs_dtor(EcsEnum)
    });

    ecs_set_hooks(world, EcsBitmask, { 
        .ctor = ecs_default_ctor,
        .move = ecs_move(EcsBitmask),
        .copy = ecs_copy(EcsBitmask),
        .dtor = ecs_dtor(EcsBitmask)
    });

    ecs_set_hooks(world, EcsUnit, { 
        .ctor = ecs_default_ctor,
        .move = ecs_move(EcsUnit),
        .copy = ecs_copy(EcsUnit),
        .dtor = ecs_dtor(EcsUnit)
    });

    ecs_set_hooks(world, EcsUnitPrefix, { 
        .ctor = ecs_default_ctor,
        .move = ecs_move(EcsUnitPrefix),
        .copy = ecs_copy(EcsUnitPrefix),
        .dtor = ecs_dtor(EcsUnitPrefix)
    });

    /* Register triggers to finalize type information from component data */
    ecs_entity_t old_scope = ecs_set_scope( /* Keep meta scope clean */
        world, EcsFlecsInternals);
    ecs_observer(world, {
        .filter.terms[0] = { .id = ecs_id(EcsPrimitive), .src.flags = EcsSelf },
        .events = {EcsOnSet},
        .callback = flecs_set_primitive
    });

    ecs_observer(world, {
        .filter.terms[0] = { .id = ecs_id(EcsMember), .src.flags = EcsSelf },
        .events = {EcsOnSet},
        .callback = flecs_set_member
    });

    ecs_observer(world, {
        .filter.terms[0] = { .id = ecs_id(EcsEnum), .src.flags = EcsSelf },
        .events = {EcsOnAdd},
        .callback = flecs_add_enum
    });

    ecs_observer(world, {
        .filter.terms[0] = { .id = ecs_id(EcsBitmask), .src.flags = EcsSelf },
        .events = {EcsOnAdd},
        .callback = flecs_add_bitmask
    });

    ecs_observer(world, {
        .filter.terms[0] = { .id = EcsConstant, .src.flags = EcsSelf },
        .events = {EcsOnAdd},
        .callback = flecs_add_constant
    });

    ecs_observer(world, {
        .filter.terms[0] = { .id = ecs_pair(EcsConstant, EcsWildcard), .src.flags = EcsSelf },
        .events = {EcsOnSet},
        .callback = flecs_add_constant
    });

    ecs_observer(world, {
        .filter.terms[0] = { .id = ecs_id(EcsArray), .src.flags = EcsSelf },
        .events = {EcsOnSet},
        .callback = flecs_set_array
    });

    ecs_observer(world, {
        .filter.terms[0] = { .id = ecs_id(EcsVector), .src.flags = EcsSelf },
        .events = {EcsOnSet},
        .callback = flecs_set_vector
    });

    ecs_observer(world, {
        .filter.terms[0] = { .id = ecs_id(EcsOpaque), .src.flags = EcsSelf },
        .events = {EcsOnSet},
        .callback = flecs_set_custom_type
    });

    ecs_observer(world, {
        .filter.terms[0] = { .id = ecs_id(EcsUnit), .src.flags = EcsSelf },
        .events = {EcsOnSet},
        .callback = flecs_set_unit
    });

    ecs_observer(world, {
        .filter.terms[0] = { .id = ecs_id(EcsMetaType), .src.flags = EcsSelf },
        .events = {EcsOnSet},
        .callback = ecs_meta_type_serialized_init
    });

    ecs_observer(world, {
        .filter.terms[0] = { .id = ecs_id(EcsMetaType), .src.flags = EcsSelf },
        .events = {EcsOnSet},
        .callback = ecs_meta_type_init_default_ctor
    });

    ecs_observer(world, {
        .filter.terms = {
            { .id = ecs_id(EcsUnit) },
            { .id = EcsQuantity }
        },
        .events = { EcsMonitor },
        .callback = flecs_unit_quantity_monitor
    });
    ecs_set_scope(world, old_scope);

    /* Initialize primitive types */
    #define ECS_PRIMITIVE(world, type, primitive_kind)\
        ecs_entity_init(world, &(ecs_entity_desc_t){\
            .id = ecs_id(ecs_##type##_t),\
            .name = #type,\
            .symbol = #type });\
        ecs_set(world, ecs_id(ecs_##type##_t), EcsPrimitive, {\
            .kind = primitive_kind\
        });

    ECS_PRIMITIVE(world, bool, EcsBool);
    ECS_PRIMITIVE(world, char, EcsChar);
    ECS_PRIMITIVE(world, byte, EcsByte);
    ECS_PRIMITIVE(world, u8, EcsU8);
    ECS_PRIMITIVE(world, u16, EcsU16);
    ECS_PRIMITIVE(world, u32, EcsU32);
    ECS_PRIMITIVE(world, u64, EcsU64);
    ECS_PRIMITIVE(world, uptr, EcsUPtr);
    ECS_PRIMITIVE(world, i8, EcsI8);
    ECS_PRIMITIVE(world, i16, EcsI16);
    ECS_PRIMITIVE(world, i32, EcsI32);
    ECS_PRIMITIVE(world, i64, EcsI64);
    ECS_PRIMITIVE(world, iptr, EcsIPtr);
    ECS_PRIMITIVE(world, f32, EcsF32);
    ECS_PRIMITIVE(world, f64, EcsF64);
    ECS_PRIMITIVE(world, string, EcsString);
    ECS_PRIMITIVE(world, entity, EcsEntity);

    #undef ECS_PRIMITIVE

    ecs_set_hooks(world, ecs_string_t, {
        .ctor = ecs_default_ctor,
        .copy = ecs_copy(ecs_string_t),
        .move = ecs_move(ecs_string_t),
        .dtor = ecs_dtor(ecs_string_t)
    });

    /* Set default child components */
    ecs_add_pair(world, ecs_id(EcsStruct), 
        EcsDefaultChildComponent, ecs_id(EcsMember));

    ecs_add_pair(world, ecs_id(EcsMember), 
        EcsDefaultChildComponent, ecs_id(EcsMember));

    ecs_add_pair(world, ecs_id(EcsEnum), 
        EcsDefaultChildComponent, EcsConstant);

    ecs_add_pair(world, ecs_id(EcsBitmask), 
        EcsDefaultChildComponent, EcsConstant);

    /* Relationship properties */
    ecs_add_id(world, EcsQuantity, EcsExclusive);
    ecs_add_id(world, EcsQuantity, EcsTag);

    /* Initialize reflection data for meta components */
    ecs_entity_t type_kind = ecs_enum_init(world, &(ecs_enum_desc_t){
        .entity = ecs_entity(world, { .name = "TypeKind" }),
        .constants = {
            {.name = "PrimitiveType"},
            {.name = "BitmaskType"},
            {.name = "EnumType"},
            {.name = "StructType"},
            {.name = "ArrayType"},
            {.name = "VectorType"},
            {.name = "OpaqueType"}
        }
    });

    ecs_struct_init(world, &(ecs_struct_desc_t){
        .entity = ecs_id(EcsMetaType),
        .members = {
            {.name = (char*)"kind", .type = type_kind}
        }
    });

    ecs_entity_t primitive_kind = ecs_enum_init(world, &(ecs_enum_desc_t){
        .entity = ecs_entity(world, { .name = "PrimitiveKind" }),
        .constants = {
            {.name = "Bool", 1}, 
            {.name = "Char"}, 
            {.name = "Byte"}, 
            {.name = "U8"}, 
            {.name = "U16"}, 
            {.name = "U32"}, 
            {.name = "U64"},
            {.name = "I8"}, 
            {.name = "I16"}, 
            {.name = "I32"}, 
            {.name = "I64"}, 
            {.name = "F32"}, 
            {.name = "F64"}, 
            {.name = "UPtr"},
            {.name = "IPtr"}, 
            {.name = "String"}, 
            {.name = "Entity"}
        }
    });

    ecs_struct_init(world, &(ecs_struct_desc_t){
        .entity = ecs_id(EcsPrimitive),
        .members = {
            {.name = (char*)"kind", .type = primitive_kind}
        }
    });

    ecs_struct_init(world, &(ecs_struct_desc_t){
        .entity = ecs_id(EcsMember),
        .members = {
            {.name = (char*)"type", .type = ecs_id(ecs_entity_t)},
            {.name = (char*)"count", .type = ecs_id(ecs_i32_t)},
            {.name = (char*)"unit", .type = ecs_id(ecs_entity_t)},
            {.name = (char*)"offset", .type = ecs_id(ecs_i32_t)}
        }
    });

    ecs_struct_init(world, &(ecs_struct_desc_t){
        .entity = ecs_id(EcsArray),
        .members = {
            {.name = (char*)"type", .type = ecs_id(ecs_entity_t)},
            {.name = (char*)"count", .type = ecs_id(ecs_i32_t)},
        }
    });

    ecs_struct_init(world, &(ecs_struct_desc_t){
        .entity = ecs_id(EcsVector),
        .members = {
            {.name = (char*)"type", .type = ecs_id(ecs_entity_t)}
        }
    });

    ecs_struct_init(world, &(ecs_struct_desc_t){
        .entity = ecs_id(EcsOpaque),
        .members = {
            { .name = (char*)"as_type", .type = ecs_id(ecs_entity_t) }
        }
    });

    ecs_entity_t ut = ecs_struct_init(world, &(ecs_struct_desc_t){
        .entity = ecs_entity(world, { .name = "unit_translation" }),
        .members = {
            {.name = (char*)"factor", .type = ecs_id(ecs_i32_t)},
            {.name = (char*)"power", .type = ecs_id(ecs_i32_t)}
        }
    });

    ecs_struct_init(world, &(ecs_struct_desc_t){
        .entity = ecs_id(EcsUnit),
        .members = {
            {.name = (char*)"symbol", .type = ecs_id(ecs_string_t)},
            {.name = (char*)"prefix", .type = ecs_id(ecs_entity_t)},
            {.name = (char*)"base", .type = ecs_id(ecs_entity_t)},
            {.name = (char*)"over", .type = ecs_id(ecs_entity_t)},
            {.name = (char*)"translation", .type = ut}
        }
    });

    ecs_struct_init(world, &(ecs_struct_desc_t){
        .entity = ecs_id(EcsUnitPrefix),
        .members = {
            {.name = (char*)"symbol", .type = ecs_id(ecs_string_t)},
            {.name = (char*)"translation", .type = ut}
        }
    });
}

#endif

/**
 * @file meta/api.c
 * @brief API for assigning values of runtime types with reflection.
 */

#include <ctype.h>

#ifdef FLECS_META

static
const char* flecs_meta_op_kind_str(
    ecs_meta_type_op_kind_t kind)
{
    switch(kind) {

    case EcsOpEnum: return "Enum";
    case EcsOpBitmask: return "Bitmask";
    case EcsOpArray: return "Array";
    case EcsOpVector: return "Vector";
    case EcsOpOpaque: return "Opaque";
    case EcsOpPush: return "Push";
    case EcsOpPop: return "Pop";
    case EcsOpPrimitive: return "Primitive";
    case EcsOpBool: return "Bool";
    case EcsOpChar: return "Char";
    case EcsOpByte: return "Byte";
    case EcsOpU8: return "U8";
    case EcsOpU16: return "U16";
    case EcsOpU32: return "U32";
    case EcsOpU64: return "U64";
    case EcsOpI8: return "I8";
    case EcsOpI16: return "I16";
    case EcsOpI32: return "I32";
    case EcsOpI64: return "I64";
    case EcsOpF32: return "F32";
    case EcsOpF64: return "F64";
    case EcsOpUPtr: return "UPtr";
    case EcsOpIPtr: return "IPtr";
    case EcsOpString: return "String";
    case EcsOpEntity: return "Entity";
    default: return "<< invalid kind >>";
    }
}

/* Get current scope */
static
ecs_meta_scope_t* flecs_meta_cursor_get_scope(
    const ecs_meta_cursor_t *cursor)
{
    ecs_check(cursor != NULL, ECS_INVALID_PARAMETER, NULL);
    return (ecs_meta_scope_t*)&cursor->scope[cursor->depth];
error:
    return NULL;
}

/* Restore scope, if dotmember was used */
static
ecs_meta_scope_t* flecs_meta_cursor_restore_scope(
    ecs_meta_cursor_t *cursor,
    const ecs_meta_scope_t* scope)
{
    ecs_check(cursor != NULL, ECS_INVALID_PARAMETER, NULL);
    ecs_check(scope != NULL, ECS_INVALID_PARAMETER, NULL);
    if (scope->prev_depth) {
        cursor->depth = scope->prev_depth;
    }
error:
    return (ecs_meta_scope_t*)&cursor->scope[cursor->depth];
}

/* Get current operation for scope */
static
ecs_meta_type_op_t* flecs_meta_cursor_get_op(
    ecs_meta_scope_t *scope)
{
    ecs_assert(scope->ops != NULL, ECS_INVALID_OPERATION, NULL);
    return &scope->ops[scope->op_cur];
}

/* Get component for type in current scope */
static
const EcsComponent* get_ecs_component(
    const ecs_world_t *world,
    ecs_meta_scope_t *scope)
{
    const EcsComponent *comp = scope->comp;
    if (!comp) {
        comp = scope->comp = ecs_get(world, scope->type, EcsComponent);
        ecs_assert(comp != NULL, ECS_INTERNAL_ERROR, NULL);
    }
    return comp;
}

/* Get size for type in current scope */
static
ecs_size_t get_size(
    const ecs_world_t *world,
    ecs_meta_scope_t *scope)
{
    return get_ecs_component(world, scope)->size;
}

static
int32_t get_elem_count(
    ecs_meta_scope_t *scope)
{
    const EcsOpaque *opaque = scope->opaque;

    if (scope->vector) {
        return ecs_vec_count(scope->vector);
    } else if (opaque && opaque->count) {
        return flecs_uto(int32_t, opaque->count(scope[-1].ptr));
    }

    ecs_meta_type_op_t *op = flecs_meta_cursor_get_op(scope);
    return op->count;
}

/* Get pointer to current field/element */
static
ecs_meta_type_op_t* flecs_meta_cursor_get_ptr(
    const ecs_world_t *world,
    ecs_meta_scope_t *scope)
{
    ecs_meta_type_op_t *op = flecs_meta_cursor_get_op(scope);
    ecs_size_t size = get_size(world, scope);
    const EcsOpaque *opaque = scope->opaque;

    if (scope->vector) {
        ecs_vec_set_min_count(NULL, scope->vector, size, scope->elem_cur + 1);
        scope->ptr = ecs_vec_first(scope->vector);
    } else if (opaque) {
        if (scope->is_collection) {
            if (!opaque->ensure_element) {
                char *str = ecs_get_fullpath(world, scope->type);
                ecs_err("missing ensure_element for opaque type %s", str);
                ecs_os_free(str);
                return NULL;
            }
            scope->is_empty_scope = false;

            void *opaque_ptr = opaque->ensure_element(
                scope->ptr, flecs_ito(size_t, scope->elem_cur));
            ecs_assert(opaque_ptr != NULL, ECS_INVALID_OPERATION, 
                "ensure_element returned NULL");
            return opaque_ptr;
        } else if (op->name) {
            if (!opaque->ensure_member) {
                char *str = ecs_get_fullpath(world, scope->type);
                ecs_err("missing ensure_member for opaque type %s", str);
                ecs_os_free(str);
                return NULL;
            }
            ecs_assert(scope->ptr != NULL, ECS_INTERNAL_ERROR, NULL);
            return opaque->ensure_member(scope->ptr, op->name);
        } else {
            ecs_err("invalid operation for opaque type");
            return NULL;
        }
    }

    return ECS_OFFSET(scope->ptr, size * scope->elem_cur + op->offset);
}

static
int flecs_meta_cursor_push_type(
    const ecs_world_t *world,
    ecs_meta_scope_t *scope,
    ecs_entity_t type,
    void *ptr)
{
    const EcsMetaTypeSerialized *ser = ecs_get(
        world, type, EcsMetaTypeSerialized);
    if (ser == NULL) {
        char *str = ecs_id_str(world, type);
        ecs_err("cannot open scope for entity '%s' which is not a type", str);
        ecs_os_free(str);
        return -1;
    }

    scope[0] = (ecs_meta_scope_t) {
        .type = type,
        .ops = ecs_vec_first_t(&ser->ops, ecs_meta_type_op_t),
        .op_count = ecs_vec_count(&ser->ops),
        .ptr = ptr
    };

    return 0;
}

ecs_meta_cursor_t ecs_meta_cursor(
    const ecs_world_t *world,
    ecs_entity_t type,
    void *ptr)
{
    ecs_check(world != NULL, ECS_INVALID_PARAMETER, NULL);
    ecs_check(type != 0, ECS_INVALID_PARAMETER, NULL);
    ecs_check(ptr != NULL, ECS_INVALID_PARAMETER, NULL);

    ecs_meta_cursor_t result = {
        .world = world,
        .valid = true
    };

    if (flecs_meta_cursor_push_type(world, result.scope, type, ptr) != 0) {
        result.valid = false;
    }

    return result;
error:
    return (ecs_meta_cursor_t){ 0 };
}

void* ecs_meta_get_ptr(
    ecs_meta_cursor_t *cursor)
{
    return flecs_meta_cursor_get_ptr(cursor->world, 
        flecs_meta_cursor_get_scope(cursor));
}

int ecs_meta_next(
    ecs_meta_cursor_t *cursor)
{
    ecs_meta_scope_t *scope = flecs_meta_cursor_get_scope(cursor);
    scope = flecs_meta_cursor_restore_scope(cursor, scope);
    ecs_meta_type_op_t *op = flecs_meta_cursor_get_op(scope);

    if (scope->is_collection) {
        scope->elem_cur ++;
        scope->op_cur = 0;

        if (scope->opaque) {
            return 0;
        }

        if (scope->elem_cur >= get_elem_count(scope)) {
            ecs_err("out of collection bounds (%d)", scope->elem_cur);
            return -1;
        }
        return 0;
    }

    scope->op_cur += op->op_count;

    if (scope->op_cur >= scope->op_count) {
        ecs_err("out of bounds");
        return -1;
    }

    return 0;
}

int ecs_meta_elem(
    ecs_meta_cursor_t *cursor,
    int32_t elem)
{
    ecs_meta_scope_t *scope = flecs_meta_cursor_get_scope(cursor);
    if (!scope->is_collection) {
        ecs_err("ecs_meta_elem can be used for collections only");
        return -1;
    }

    scope->elem_cur = elem;
    scope->op_cur = 0;

    if (scope->elem_cur >= get_elem_count(scope) || (scope->elem_cur < 0)) {
        ecs_err("out of collection bounds (%d)", scope->elem_cur);
        return -1;
    }
    
    return 0;
}

int ecs_meta_member(
    ecs_meta_cursor_t *cursor,
    const char *name)
{
    if (cursor->depth == 0) {
        ecs_err("cannot move to member in root scope");
        return -1;
    }

    ecs_meta_scope_t *scope = flecs_meta_cursor_get_scope(cursor);
    scope = flecs_meta_cursor_restore_scope(cursor, scope);

    ecs_hashmap_t *members = scope->members;
    const ecs_world_t *world = cursor->world;

    if (!members) {
        ecs_err("cannot move to member '%s' for non-struct type", name);
        return -1;
    }

    const uint64_t *cur_ptr = flecs_name_index_find_ptr(members, name, 0, 0);
    if (!cur_ptr) {
        char *path = ecs_get_fullpath(world, scope->type);
        ecs_err("unknown member '%s' for type '%s'", name, path);
        ecs_os_free(path);
        return -1;
    }

    scope->op_cur = flecs_uto(int32_t, cur_ptr[0]);

    const EcsOpaque *opaque = scope->opaque;
    if (opaque) {
        if (!opaque->ensure_member) {
            char *str = ecs_get_fullpath(world, scope->type);
            ecs_err("missing ensure_member for opaque type %s", str);
            ecs_os_free(str);
        }
    }

    return 0;
}

int ecs_meta_dotmember(
    ecs_meta_cursor_t *cursor,
    const char *name)
{
#ifdef FLECS_PARSER
    ecs_meta_scope_t *cur_scope = flecs_meta_cursor_get_scope(cursor);
    flecs_meta_cursor_restore_scope(cursor, cur_scope);

    int32_t prev_depth = cursor->depth;
    int dotcount = 0;

    char token[ECS_MAX_TOKEN_SIZE];
    const char *ptr = name;
    while ((ptr = ecs_parse_token(NULL, NULL, ptr, token, '.'))) {
        if (ptr[0] != '.' && ptr[0]) {
            ecs_parser_error(NULL, name, ptr - name, 
                "expected '.' or end of string");
            goto error;
        }

        if (dotcount) {
            ecs_meta_push(cursor);
        }

        if (ecs_meta_member(cursor, token)) {
            goto error;
        }

        if (!ptr[0]) {
            break;   
        }

        ptr ++; /* Skip . */

        dotcount ++;
    }

    cur_scope = flecs_meta_cursor_get_scope(cursor);
    if (dotcount) {
        cur_scope->prev_depth = prev_depth;
    }

    return 0;
error:
    return -1;
#else
    (void)cursor;
    (void)name;
    ecs_err("the FLECS_PARSER addon is required for ecs_meta_dotmember");
    return -1;
#endif
}

int ecs_meta_push(
    ecs_meta_cursor_t *cursor)
{
    ecs_meta_scope_t *scope = flecs_meta_cursor_get_scope(cursor);
    ecs_meta_type_op_t *op = flecs_meta_cursor_get_op(scope);
    const ecs_world_t *world = cursor->world;

    if (cursor->depth == 0) {
        if (!cursor->is_primitive_scope) {
            if (op->kind > EcsOpScope) {
                cursor->is_primitive_scope = true;
                return 0;
            }
        }
    }

    void *ptr = flecs_meta_cursor_get_ptr(world, scope);
    cursor->depth ++;
    ecs_check(cursor->depth < ECS_META_MAX_SCOPE_DEPTH,
        ECS_INVALID_PARAMETER, NULL);

    ecs_meta_scope_t *next_scope = flecs_meta_cursor_get_scope(cursor);

    /* If we're not already in an inline array and this operation is an inline
     * array, push a frame for the array.
     * Doing this first ensures that inline arrays take precedence over other
     * kinds of push operations, such as for a struct element type. */
    if (!scope->is_inline_array && op->count > 1 && !scope->is_collection) {
        /* Push a frame just for the element type, with inline_array = true */
        next_scope[0] = (ecs_meta_scope_t){
            .ops = op,
            .op_count = op->op_count,
            .ptr = scope->ptr,
            .type = op->type,
            .is_collection = true,
            .is_inline_array = true
        };

        /* With 'is_inline_array' set to true we ensure that we can never push
         * the same inline array twice */
        return 0;
    }

    /* Operation-specific switch behavior */
    switch(op->kind) {

    /* Struct push: this happens when pushing a struct member. */
    case EcsOpPush: {
        const EcsOpaque *opaque = scope->opaque;
        if (opaque) {
            /* If this is a nested push for an opaque type, push the type of the
             * element instead of the next operation. This ensures that we won't
             * use flattened offsets for nested members. */
            if (flecs_meta_cursor_push_type(
                world, next_scope, op->type, ptr) != 0) 
            {
                goto error;
            }

            /* Strip the Push operation since we already pushed */
            next_scope->members = next_scope->ops[0].members;
            next_scope->ops = &next_scope->ops[1];
            next_scope->op_count --;
            break;
        }

        /* The ops array contains a flattened list for all members and nested
         * members of a struct, so we can use (ops + 1) to initialize the ops
         * array of the next scope. */
        next_scope[0] = (ecs_meta_scope_t) {
            .ops = &op[1],                /* op after push */
            .op_count = op->op_count - 1, /* don't include pop */
            .ptr = scope->ptr,
            .type = op->type,
            .members = op->members
        };
        break;
    }

    /* Array push for an array type. Arrays can be encoded in 2 ways: either by
     * setting the EcsMember::count member to a value >1, or by specifying an
     * array type as member type. This is the latter case. */
    case EcsOpArray: {
        if (flecs_meta_cursor_push_type(world, next_scope, op->type, ptr) != 0) {
            goto error;
        }

        const EcsArray *type_ptr = ecs_get(world, op->type, EcsArray);
        next_scope->type = type_ptr->type;
        next_scope->is_collection = true;
        break;
    }

    /* Vector push */
    case EcsOpVector: {
        next_scope->vector = ptr;
        if (flecs_meta_cursor_push_type(world, next_scope, op->type, NULL) != 0) {
            goto error;
        }

        const EcsVector *type_ptr = ecs_get(world, op->type, EcsVector);
        next_scope->type = type_ptr->type;
        next_scope->is_collection = true;
        break;
    }

    /* Opaque type push. Depending on the type the opaque type represents the
     * scope will be pushed as a struct or collection type. The type information
     * of the as_type is retained, as this is important for type checking and
     * for nested opaque type support. */
    case EcsOpOpaque: {
        const EcsOpaque *type_ptr = ecs_get(world, op->type, EcsOpaque);
        ecs_entity_t as_type = type_ptr->as_type;
        const EcsMetaType *mtype_ptr = ecs_get(world, as_type, EcsMetaType);

        /* Check what kind of type the opaque type represents */
        switch(mtype_ptr->kind) {

        /* Opaque vector support */
        case EcsVectorType: {
            const EcsVector *vt = ecs_get(world, type_ptr->as_type, EcsVector);
            next_scope->type = vt->type;

            /* Push the element type of the vector type */
            if (flecs_meta_cursor_push_type(
                world, next_scope, vt->type, NULL) != 0) 
            {
                goto error;
            }

            /* This tracks whether any data was assigned inside the scope. When
             * the scope is popped, and is_empty_scope is still true, the vector
             * will be resized to 0. */
            next_scope->is_empty_scope = true;
            next_scope->is_collection = true;
            break;
        }

        /* Opaque array support */
        case EcsArrayType: {
            const EcsArray *at = ecs_get(world, type_ptr->as_type, EcsArray);
            next_scope->type = at->type;

            /* Push the element type of the array type */
            if (flecs_meta_cursor_push_type(
                world, next_scope, at->type, NULL) != 0) 
            {
                goto error;
            }

            /* Arrays are always a fixed size */
            next_scope->is_empty_scope = false;
            next_scope->is_collection = true;
            break;
        }

        /* Opaque struct support */
        case EcsStructType:
            /* Push struct type that represents the opaque type. This ensures 
             * that the deserializer retains information about members and 
             * member types, which is necessary for nested opaque types, and 
             * allows for error checking. */
            if (flecs_meta_cursor_push_type(
                world, next_scope, as_type, NULL) != 0) 
            {
                goto error;
            }

            /* Strip push op, since we already pushed */
            next_scope->members = next_scope->ops[0].members;
            next_scope->ops = &next_scope->ops[1];
            next_scope->op_count --;
            break;

        default:
            break;
        }

        next_scope->ptr = ptr;
        next_scope->opaque = type_ptr;
        break;
    }

    default: {
        char *path = ecs_get_fullpath(world, scope->type);
        ecs_err("invalid push for type '%s'", path);
        ecs_os_free(path);
        goto error;
    }
    }

    if (scope->is_collection && !scope->opaque) {
        next_scope->ptr = ECS_OFFSET(next_scope->ptr,
            scope->elem_cur * get_size(world, scope));
    }

    return 0;
error:
    return -1;
}

int ecs_meta_pop(
    ecs_meta_cursor_t *cursor)
{
    if (cursor->is_primitive_scope) {
        cursor->is_primitive_scope = false;
        return 0;
    }

    ecs_meta_scope_t *scope = flecs_meta_cursor_get_scope(cursor);
    scope = flecs_meta_cursor_restore_scope(cursor, scope);
    cursor->depth --;
    if (cursor->depth < 0) {
        ecs_err("unexpected end of scope");
        return -1;
    }

    ecs_meta_scope_t *next_scope = flecs_meta_cursor_get_scope(cursor);
    ecs_meta_type_op_t *op = flecs_meta_cursor_get_op(next_scope);

    if (!scope->is_inline_array) {
        if (op->kind == EcsOpPush) {
            next_scope->op_cur += op->op_count - 1;

            /* push + op_count should point to the operation after pop */
            op = flecs_meta_cursor_get_op(next_scope);
            ecs_assert(op->kind == EcsOpPop, ECS_INTERNAL_ERROR, NULL);
        } else if (op->kind == EcsOpArray || op->kind == EcsOpVector) {
            /* Collection type, nothing else to do */
        } else if (op->kind == EcsOpOpaque) {
            const EcsOpaque *opaque = scope->opaque;
            if (scope->is_collection) {
                const EcsMetaType *mtype = ecs_get(cursor->world, 
                    opaque->as_type, EcsMetaType);
                ecs_assert(mtype != NULL, ECS_INTERNAL_ERROR, NULL);

                /* When popping a opaque collection type, call resize to make 
                 * sure the vector isn't larger than the number of elements we
                 * deserialized. 
                 * If the opaque type represents an array, don't call resize. */
                if (mtype->kind != EcsArrayType) {
                    ecs_assert(opaque != NULL, ECS_INTERNAL_ERROR, NULL);
                    if (!opaque->resize) {
                        char *str = ecs_get_fullpath(cursor->world, scope->type);
                        ecs_err("missing resize for opaque type %s", str);
                        ecs_os_free(str);
                        return -1;
                    }
                    if (scope->is_empty_scope) {
                        /* If no values were serialized for scope, resize 
                        * collection to 0 elements. */
                        ecs_assert(!scope->elem_cur, ECS_INTERNAL_ERROR, NULL);
                        opaque->resize(scope->ptr, 0);
                    } else {
                        /* Otherwise resize collection to the index of the last
                        * deserialized element + 1 */
                        opaque->resize(scope->ptr, 
                            flecs_ito(size_t, scope->elem_cur + 1));
                    }
                }
            } else {
                /* Opaque struct type, nothing to be done */
            }
        } else {
            /* should not have been able to push if the previous scope was not
             * a complex or collection type */
            ecs_assert(false, ECS_INTERNAL_ERROR, NULL);
        }
    } else {
        /* Make sure that this was an inline array */
        ecs_assert(next_scope->op_count > 1, ECS_INTERNAL_ERROR, NULL);
    }

    return 0;
}

bool ecs_meta_is_collection(
    const ecs_meta_cursor_t *cursor)
{
    ecs_meta_scope_t *scope = flecs_meta_cursor_get_scope(cursor);
    return scope->is_collection;
}

ecs_entity_t ecs_meta_get_type(
    const ecs_meta_cursor_t *cursor)
{
    ecs_meta_scope_t *scope = flecs_meta_cursor_get_scope(cursor);
    ecs_meta_type_op_t *op = flecs_meta_cursor_get_op(scope);
    return op->type;
}

ecs_entity_t ecs_meta_get_unit(
    const ecs_meta_cursor_t *cursor)
{
    ecs_meta_scope_t *scope = flecs_meta_cursor_get_scope(cursor);
    ecs_meta_type_op_t *op = flecs_meta_cursor_get_op(scope);
    return op->unit;
}

const char* ecs_meta_get_member(
    const ecs_meta_cursor_t *cursor)
{
    ecs_meta_scope_t *scope = flecs_meta_cursor_get_scope(cursor);
    ecs_meta_type_op_t *op = flecs_meta_cursor_get_op(scope);
    return op->name;
}

/* Utilities for type conversions and bounds checking */
struct {
    int64_t min, max;
} ecs_meta_bounds_signed[EcsMetaTypeOpKindLast + 1] = {
    [EcsOpBool]    = {false,      true},
    [EcsOpChar]    = {INT8_MIN,   INT8_MAX},
    [EcsOpByte]    = {0,          UINT8_MAX},
    [EcsOpU8]      = {0,          UINT8_MAX},
    [EcsOpU16]     = {0,          UINT16_MAX},
    [EcsOpU32]     = {0,          UINT32_MAX},
    [EcsOpU64]     = {0,          INT64_MAX},
    [EcsOpI8]      = {INT8_MIN,   INT8_MAX},
    [EcsOpI16]     = {INT16_MIN,  INT16_MAX},
    [EcsOpI32]     = {INT32_MIN,  INT32_MAX},
    [EcsOpI64]     = {INT64_MIN,  INT64_MAX},
    [EcsOpUPtr]    = {0, ((sizeof(void*) == 4) ? UINT32_MAX : INT64_MAX)},
    [EcsOpIPtr]    = {
        ((sizeof(void*) == 4) ? INT32_MIN : INT64_MIN), 
        ((sizeof(void*) == 4) ? INT32_MAX : INT64_MAX)
    },
    [EcsOpEntity]  = {0,          INT64_MAX},
    [EcsOpEnum]    = {INT32_MIN,  INT32_MAX},
    [EcsOpBitmask] = {0,          INT32_MAX}
};

struct {
    uint64_t min, max;
} ecs_meta_bounds_unsigned[EcsMetaTypeOpKindLast + 1] = {
    [EcsOpBool]    = {false,      true},
    [EcsOpChar]    = {0,          INT8_MAX},
    [EcsOpByte]    = {0,          UINT8_MAX},
    [EcsOpU8]      = {0,          UINT8_MAX},
    [EcsOpU16]     = {0,          UINT16_MAX},
    [EcsOpU32]     = {0,          UINT32_MAX},
    [EcsOpU64]     = {0,          UINT64_MAX},
    [EcsOpI8]      = {0,          INT8_MAX},
    [EcsOpI16]     = {0,          INT16_MAX},
    [EcsOpI32]     = {0,          INT32_MAX},
    [EcsOpI64]     = {0,          INT64_MAX},
    [EcsOpUPtr]    = {0, ((sizeof(void*) == 4) ? UINT32_MAX : UINT64_MAX)},
    [EcsOpIPtr]    = {0, ((sizeof(void*) == 4) ? INT32_MAX : INT64_MAX)},
    [EcsOpEntity]  = {0,          UINT64_MAX},
    [EcsOpEnum]    = {0,          INT32_MAX},
    [EcsOpBitmask] = {0,          UINT32_MAX}
};

struct {
    double min, max;
} ecs_meta_bounds_float[EcsMetaTypeOpKindLast + 1] = {
    [EcsOpBool]    = {false,      true},
    [EcsOpChar]    = {INT8_MIN,   INT8_MAX},
    [EcsOpByte]    = {0,          UINT8_MAX},
    [EcsOpU8]      = {0,          UINT8_MAX},
    [EcsOpU16]     = {0,          UINT16_MAX},
    [EcsOpU32]     = {0,          UINT32_MAX},
    [EcsOpU64]     = {0,          (double)UINT64_MAX},
    [EcsOpI8]      = {INT8_MIN,   INT8_MAX},
    [EcsOpI16]     = {INT16_MIN,  INT16_MAX},
    [EcsOpI32]     = {INT32_MIN,  INT32_MAX},
    [EcsOpI64]     = {INT64_MIN,  (double)INT64_MAX},
    [EcsOpUPtr]    = {0, ((sizeof(void*) == 4) ? UINT32_MAX : (double)UINT64_MAX)},
    [EcsOpIPtr]    = {
        ((sizeof(void*) == 4) ? INT32_MIN : (double)INT64_MIN), 
        ((sizeof(void*) == 4) ? INT32_MAX : (double)INT64_MAX)
    },
    [EcsOpEntity]  = {0,          (double)UINT64_MAX},
    [EcsOpEnum]    = {INT32_MIN,  INT32_MAX},
    [EcsOpBitmask] = {0,          UINT32_MAX}
};

#define set_T(T, ptr, value)\
    ((T*)ptr)[0] = ((T)value)

#define case_T(kind, T, dst, src)\
case kind:\
    set_T(T, dst, src);\
    break

#define case_T_checked(kind, T, dst, src, bounds)\
case kind:\
    if ((src < bounds[kind].min) || (src > bounds[kind].max)){\
        ecs_err("value %.0f is out of bounds for type %s", (double)src,\
            flecs_meta_op_kind_str(kind));\
        return -1;\
    }\
    set_T(T, dst, src);\
    break

#define cases_T_float(dst, src)\
    case_T(EcsOpF32,  ecs_f32_t,  dst, src);\
    case_T(EcsOpF64,  ecs_f64_t,  dst, src)

#define cases_T_signed(dst, src, bounds)\
    case_T_checked(EcsOpChar, ecs_char_t, dst, src, bounds);\
    case_T_checked(EcsOpI8,   ecs_i8_t,   dst, src, bounds);\
    case_T_checked(EcsOpI16,  ecs_i16_t,  dst, src, bounds);\
    case_T_checked(EcsOpI32,  ecs_i32_t,  dst, src, bounds);\
    case_T_checked(EcsOpI64,  ecs_i64_t,  dst, src, bounds);\
    case_T_checked(EcsOpIPtr, ecs_iptr_t, dst, src, bounds);\
    case_T_checked(EcsOpEnum, ecs_i32_t, dst, src, bounds)

#define cases_T_unsigned(dst, src, bounds)\
    case_T_checked(EcsOpByte, ecs_byte_t, dst, src, bounds);\
    case_T_checked(EcsOpU8,   ecs_u8_t,   dst, src, bounds);\
    case_T_checked(EcsOpU16,  ecs_u16_t,  dst, src, bounds);\
    case_T_checked(EcsOpU32,  ecs_u32_t,  dst, src, bounds);\
    case_T_checked(EcsOpU64,  ecs_u64_t,  dst, src, bounds);\
    case_T_checked(EcsOpUPtr, ecs_uptr_t, dst, src, bounds);\
    case_T_checked(EcsOpEntity, ecs_u64_t, dst, src, bounds);\
    case_T_checked(EcsOpBitmask, ecs_u32_t, dst, src, bounds)

#define cases_T_bool(dst, src)\
case EcsOpBool:\
    set_T(ecs_bool_t, dst, value != 0);\
    break

static
void flecs_meta_conversion_error(
    ecs_meta_cursor_t *cursor,
    ecs_meta_type_op_t *op,
    const char *from)
{
    if (op->kind == EcsOpPop) {
        ecs_err("cursor: out of bounds");
    } else {
        char *path = ecs_get_fullpath(cursor->world, op->type);
        ecs_err("unsupported conversion from %s to '%s'", from, path);
        ecs_os_free(path);
    }
}

int ecs_meta_set_bool(
    ecs_meta_cursor_t *cursor,
    bool value)
{
    ecs_meta_scope_t *scope = flecs_meta_cursor_get_scope(cursor);
    ecs_meta_type_op_t *op = flecs_meta_cursor_get_op(scope);
    void *ptr = flecs_meta_cursor_get_ptr(cursor->world, scope);

    switch(op->kind) {
    cases_T_bool(ptr, value);
    cases_T_signed(ptr, value, ecs_meta_bounds_signed);
    cases_T_unsigned(ptr, value, ecs_meta_bounds_unsigned);
    case EcsOpOpaque: {
        const EcsOpaque *ot = ecs_get(cursor->world, op->type, EcsOpaque);
        if (ot && ot->assign_bool) {
            ot->assign_bool(ptr, value);
            break;
        }
    }
    /* fall through */
    default:
        flecs_meta_conversion_error(cursor, op, "bool");
        return -1;
    }

    return 0;
}

int ecs_meta_set_char(
    ecs_meta_cursor_t *cursor,
    char value)
{
    ecs_meta_scope_t *scope = flecs_meta_cursor_get_scope(cursor);
    ecs_meta_type_op_t *op = flecs_meta_cursor_get_op(scope);
    void *ptr = flecs_meta_cursor_get_ptr(cursor->world, scope);

    switch(op->kind) {
    cases_T_bool(ptr, value);
    cases_T_signed(ptr, value, ecs_meta_bounds_signed);
    case EcsOpOpaque: {
        const EcsOpaque *opaque = ecs_get(cursor->world, op->type, EcsOpaque);
        ecs_assert(opaque != NULL, ECS_INVALID_OPERATION, NULL);
        if (opaque->assign_char) { /* preferred operation */
            opaque->assign_char(ptr, value);
            break;
        } else if (opaque->assign_uint) {
            opaque->assign_uint(ptr, (uint64_t)value);
            break;
        } else if (opaque->assign_int) {
            opaque->assign_int(ptr, value);
            break;
        }
    }
    /* fall through */
    default:
        flecs_meta_conversion_error(cursor, op, "char");
        return -1;
    }

    return 0;
}

int ecs_meta_set_int(
    ecs_meta_cursor_t *cursor,
    int64_t value)
{
    ecs_meta_scope_t *scope = flecs_meta_cursor_get_scope(cursor);
    ecs_meta_type_op_t *op = flecs_meta_cursor_get_op(scope);
    void *ptr = flecs_meta_cursor_get_ptr(cursor->world, scope);

    switch(op->kind) {
    cases_T_bool(ptr, value);
    cases_T_signed(ptr, value, ecs_meta_bounds_signed);
    cases_T_unsigned(ptr, value, ecs_meta_bounds_signed);
    cases_T_float(ptr, value);
    case EcsOpOpaque: {
        const EcsOpaque *opaque = ecs_get(cursor->world, op->type, EcsOpaque);
        ecs_assert(opaque != NULL, ECS_INVALID_OPERATION, NULL);
        if (opaque->assign_int) { /* preferred operation */
            opaque->assign_int(ptr, value);
            break;
        } else if (opaque->assign_float) { /* most expressive */
            opaque->assign_float(ptr, (double)value);
            break;
        } else if (opaque->assign_uint && (value > 0)) {
            opaque->assign_uint(ptr, flecs_ito(uint64_t, value));
            break;
        } else if (opaque->assign_char && (value > 0) && (value < 256)) {
            opaque->assign_char(ptr, flecs_ito(char, value));
            break;
        }
    }
    /* fall through */
    default: {
        if(!value) return ecs_meta_set_null(cursor);
        flecs_meta_conversion_error(cursor, op, "int");
        return -1;
    }
    }

    return 0;
}

int ecs_meta_set_uint(
    ecs_meta_cursor_t *cursor,
    uint64_t value)
{
    ecs_meta_scope_t *scope = flecs_meta_cursor_get_scope(cursor);
    ecs_meta_type_op_t *op = flecs_meta_cursor_get_op(scope);
    void *ptr = flecs_meta_cursor_get_ptr(cursor->world, scope);

    switch(op->kind) {
    cases_T_bool(ptr, value);
    cases_T_signed(ptr, value, ecs_meta_bounds_unsigned);
    cases_T_unsigned(ptr, value, ecs_meta_bounds_unsigned);
    cases_T_float(ptr, value);
    case EcsOpOpaque: {
        const EcsOpaque *opaque = ecs_get(cursor->world, op->type, EcsOpaque);
        ecs_assert(opaque != NULL, ECS_INVALID_OPERATION, NULL);
        if (opaque->assign_uint) { /* preferred operation */
            opaque->assign_uint(ptr, value);
            break;
        } else if (opaque->assign_float) { /* most expressive */
            opaque->assign_float(ptr, (double)value);
            break;
        } else if (opaque->assign_int && (value < INT64_MAX)) {
            opaque->assign_int(ptr, flecs_uto(int64_t, value));
            break;
        } else if (opaque->assign_char && (value < 256)) {
            opaque->assign_char(ptr, flecs_uto(char, value));
            break;
        }
    }
    /* fall through */
    default:
        if(!value) return ecs_meta_set_null(cursor);
        flecs_meta_conversion_error(cursor, op, "uint");
        return -1;
    }

    return 0;
}

int ecs_meta_set_float(
    ecs_meta_cursor_t *cursor,
    double value)
{
    ecs_meta_scope_t *scope = flecs_meta_cursor_get_scope(cursor);
    ecs_meta_type_op_t *op = flecs_meta_cursor_get_op(scope);
    void *ptr = flecs_meta_cursor_get_ptr(cursor->world, scope);

    switch(op->kind) {
    cases_T_bool(ptr, value);
    cases_T_signed(ptr, value, ecs_meta_bounds_float);
    cases_T_unsigned(ptr, value, ecs_meta_bounds_float);
    cases_T_float(ptr, value);
    case EcsOpOpaque: {
        const EcsOpaque *opaque = ecs_get(cursor->world, op->type, EcsOpaque);
        ecs_assert(opaque != NULL, ECS_INVALID_OPERATION, NULL);
        if (opaque->assign_float) { /* preferred operation */
            opaque->assign_float(ptr, value);
            break;
        } else if (opaque->assign_int && /* most expressive */
            (value <= (double)INT64_MAX) && (value >= (double)INT64_MIN)) 
        {
            opaque->assign_int(ptr, (int64_t)value);
            break;
        } else if (opaque->assign_uint && (value >= 0)) {
            opaque->assign_uint(ptr, (uint64_t)value);
            break;
        } else if (opaque->assign_entity && (value >= 0)) {
            opaque->assign_entity(
                ptr, (ecs_world_t*)cursor->world, (ecs_entity_t)value);
            break;
        }
    }
    /* fall through */
    default:
        flecs_meta_conversion_error(cursor, op, "float");
        return -1;
    }

    return 0;
}

int ecs_meta_set_value(
    ecs_meta_cursor_t *cursor,
    const ecs_value_t *value)
{
    ecs_check(value != NULL, ECS_INVALID_PARAMETER, NULL);
    ecs_entity_t type = value->type;
    ecs_check(type != 0, ECS_INVALID_PARAMETER, NULL);
    const EcsMetaType *mt = ecs_get(cursor->world, type, EcsMetaType);
    if (!mt) {
        ecs_err("type of value does not have reflection data");
        return -1;
    }

    if (mt->kind == EcsPrimitiveType) {
        const EcsPrimitive *prim = ecs_get(cursor->world, type, EcsPrimitive);
        ecs_check(prim != NULL, ECS_INTERNAL_ERROR, NULL);
        switch(prim->kind) {
        case EcsBool: return ecs_meta_set_bool(cursor, *(bool*)value->ptr);
        case EcsChar: return ecs_meta_set_char(cursor, *(char*)value->ptr);
        case EcsByte: return ecs_meta_set_uint(cursor, *(uint8_t*)value->ptr);
        case EcsU8:   return ecs_meta_set_uint(cursor, *(uint8_t*)value->ptr);
        case EcsU16:  return ecs_meta_set_uint(cursor, *(uint16_t*)value->ptr);
        case EcsU32:  return ecs_meta_set_uint(cursor, *(uint32_t*)value->ptr);
        case EcsU64:  return ecs_meta_set_uint(cursor, *(uint64_t*)value->ptr);
        case EcsI8:   return ecs_meta_set_int(cursor, *(int8_t*)value->ptr);
        case EcsI16:  return ecs_meta_set_int(cursor, *(int16_t*)value->ptr);
        case EcsI32:  return ecs_meta_set_int(cursor, *(int32_t*)value->ptr);
        case EcsI64:  return ecs_meta_set_int(cursor, *(int64_t*)value->ptr);
        case EcsF32:  return ecs_meta_set_float(cursor, (double)*(float*)value->ptr);
        case EcsF64:  return ecs_meta_set_float(cursor, *(double*)value->ptr);
        case EcsUPtr: return ecs_meta_set_uint(cursor, *(uintptr_t*)value->ptr);
        case EcsIPtr: return ecs_meta_set_int(cursor, *(intptr_t*)value->ptr);
        case EcsString: return ecs_meta_set_string(cursor, *(char**)value->ptr);
        case EcsEntity: return ecs_meta_set_entity(cursor, 
            *(ecs_entity_t*)value->ptr);
        default:
            ecs_throw(ECS_INTERNAL_ERROR, "invalid type kind");
            goto error;
        }
    } else if (mt->kind == EcsEnumType) {
        return ecs_meta_set_int(cursor, *(int32_t*)value->ptr);
    } else if (mt->kind == EcsBitmaskType) {
        return ecs_meta_set_int(cursor, *(uint32_t*)value->ptr);
    } else {
        ecs_meta_scope_t *scope = flecs_meta_cursor_get_scope(cursor);
        ecs_meta_type_op_t *op = flecs_meta_cursor_get_op(scope);
        void *ptr = flecs_meta_cursor_get_ptr(cursor->world, scope);
        if (op->type != value->type) {
            char *type_str = ecs_get_fullpath(cursor->world, value->type);
            flecs_meta_conversion_error(cursor, op, type_str);
            ecs_os_free(type_str);
            goto error;
        }
        return ecs_value_copy(cursor->world, value->type, ptr, value->ptr);
    }

error:
    return -1;
}

static
int flecs_meta_add_bitmask_constant(
    ecs_meta_cursor_t *cursor,
    ecs_meta_type_op_t *op,
    void *out,
    const char *value)
{
    ecs_assert(op->type != 0, ECS_INTERNAL_ERROR, NULL);

    if (!ecs_os_strcmp(value, "0")) {
        return 0;
    }

    ecs_entity_t c = ecs_lookup_child(cursor->world, op->type, value);
    if (!c) {
        char *path = ecs_get_fullpath(cursor->world, op->type);
        ecs_err("unresolved bitmask constant '%s' for type '%s'", value, path);
        ecs_os_free(path);
        return -1;
    }

    const ecs_u32_t *v = ecs_get_pair_object(
        cursor->world, c, EcsConstant, ecs_u32_t);
    if (v == NULL) {
        char *path = ecs_get_fullpath(cursor->world, op->type);
        ecs_err("'%s' is not an bitmask constant for type '%s'", value, path);
        ecs_os_free(path);
        return -1;
    }

    *(ecs_u32_t*)out |= v[0];

    return 0;
}

static
int flecs_meta_parse_bitmask(
    ecs_meta_cursor_t *cursor,
    ecs_meta_type_op_t *op,
    void *out,
    const char *value)
{
    char token[ECS_MAX_TOKEN_SIZE];

    const char *prev = value, *ptr = value;

    *(ecs_u32_t*)out = 0;

    while ((ptr = strchr(ptr, '|'))) {
        ecs_os_memcpy(token, prev, ptr - prev);
        token[ptr - prev] = '\0';
        if (flecs_meta_add_bitmask_constant(cursor, op, out, token) != 0) {
            return -1;
        }

        ptr ++;
        prev = ptr;
    }

    if (flecs_meta_add_bitmask_constant(cursor, op, out, prev) != 0) {
        return -1;
    }

    return 0;
}

static
int flecs_meta_cursor_lookup(
    ecs_meta_cursor_t *cursor,
    const char *value,
    ecs_entity_t *out)
{
    if (ecs_os_strcmp(value, "0")) {
        if (cursor->lookup_action) {
            *out = cursor->lookup_action(
                cursor->world, value,
                cursor->lookup_ctx);
        } else {
            *out = ecs_lookup_path(cursor->world, 0, value);
        }
        if (!*out) {
            ecs_err("unresolved entity identifier '%s'", value);
            return -1;
        }
    }
    return 0;
}

int ecs_meta_set_string(
    ecs_meta_cursor_t *cursor,
    const char *value)
{
    ecs_meta_scope_t *scope = flecs_meta_cursor_get_scope(cursor);
    ecs_meta_type_op_t *op = flecs_meta_cursor_get_op(scope);
    void *ptr = flecs_meta_cursor_get_ptr(cursor->world, scope);

    switch(op->kind) {
    case EcsOpBool:
        if (!ecs_os_strcmp(value, "true")) {
            set_T(ecs_bool_t, ptr, true);
        } else if (!ecs_os_strcmp(value, "false")) {
            set_T(ecs_bool_t, ptr, false);
        } else if (isdigit(value[0])) {
            if (!ecs_os_strcmp(value, "0")) {
                set_T(ecs_bool_t, ptr, false);
            } else {
                set_T(ecs_bool_t, ptr, true);
            }
        } else {
            ecs_err("invalid value for boolean '%s'", value);
            return -1;
        }
        break;
    case EcsOpI8:
    case EcsOpU8:
    case EcsOpByte:
        set_T(ecs_i8_t, ptr, atol(value));
        break;
    case EcsOpChar:
        set_T(char, ptr, value[0]);
        break;
    case EcsOpI16:
    case EcsOpU16:
        set_T(ecs_i16_t, ptr, atol(value));
        break;
    case EcsOpI32:
    case EcsOpU32:
        set_T(ecs_i32_t, ptr, atol(value));
        break;
    case EcsOpI64:
    case EcsOpU64:
        set_T(ecs_i64_t, ptr, atol(value));
        break;
    case EcsOpIPtr:
    case EcsOpUPtr:
        set_T(ecs_iptr_t, ptr, atol(value));
        break;
    case EcsOpF32:
        set_T(ecs_f32_t, ptr, atof(value));
        break;
    case EcsOpF64:
        set_T(ecs_f64_t, ptr, atof(value));
        break;
    case EcsOpString: {
        ecs_assert(*(ecs_string_t*)ptr != value, ECS_INVALID_PARAMETER, NULL);
        ecs_os_free(*(ecs_string_t*)ptr);
        char *result = ecs_os_strdup(value);
        set_T(ecs_string_t, ptr, result);
        break;
    }
    case EcsOpEnum: {
        ecs_assert(op->type != 0, ECS_INTERNAL_ERROR, NULL);
        ecs_entity_t c = ecs_lookup_child(cursor->world, op->type, value);
        if (!c) {
            char *path = ecs_get_fullpath(cursor->world, op->type);
            ecs_err("unresolved enum constant '%s' for type '%s'", value, path);
            ecs_os_free(path);
            return -1;
        }

        const ecs_i32_t *v = ecs_get_pair_object(
            cursor->world, c, EcsConstant, ecs_i32_t);
        if (v == NULL) {
            char *path = ecs_get_fullpath(cursor->world, op->type);
            ecs_err("'%s' is not an enum constant for type '%s'", value, path);
            ecs_os_free(path);
            return -1;
        }

        set_T(ecs_i32_t, ptr, v[0]);
        break;
    }
    case EcsOpBitmask:
        if (flecs_meta_parse_bitmask(cursor, op, ptr, value) != 0) {
            return -1;
        }
        break;
    case EcsOpEntity: {
        ecs_entity_t e = 0;
        if (flecs_meta_cursor_lookup(cursor, value, &e)) {
            return -1;
        }
        set_T(ecs_entity_t, ptr, e);
        break;
    }
    case EcsOpPop:
        ecs_err("excess element '%s' in scope", value);
        return -1;
    case EcsOpOpaque: {
        const EcsOpaque *opaque = ecs_get(cursor->world, op->type, EcsOpaque);
        ecs_assert(opaque != NULL, ECS_INVALID_OPERATION, NULL);
        if (opaque->assign_string) { /* preferred */
            opaque->assign_string(ptr, value);
            break;
        } else if (opaque->assign_char && value[0] && !value[1]) {
            opaque->assign_char(ptr, value[0]);
            break;
        } else if (opaque->assign_entity) {
            ecs_entity_t e = 0;
            if (flecs_meta_cursor_lookup(cursor, value, &e)) {
                return -1;
            }
            opaque->assign_entity(ptr, (ecs_world_t*)cursor->world, e);
            break;
        }
    }
    /* fall through */
    default:
        ecs_err("unsupported conversion from string '%s' to '%s'",
            value, flecs_meta_op_kind_str(op->kind));
        return -1;
    }

    return 0;
}

int ecs_meta_set_string_literal(
    ecs_meta_cursor_t *cursor,
    const char *value)
{
    ecs_meta_scope_t *scope = flecs_meta_cursor_get_scope(cursor);
    ecs_meta_type_op_t *op = flecs_meta_cursor_get_op(scope);
    void *ptr = flecs_meta_cursor_get_ptr(cursor->world, scope);

    ecs_size_t len = ecs_os_strlen(value);
    if (value[0] != '\"' || value[len - 1] != '\"') {
        ecs_err("invalid string literal '%s'", value);
        return -1;
    }

    switch(op->kind) {
    case EcsOpChar:
        set_T(ecs_char_t, ptr, value[1]);
        break;

    default:
    case EcsOpEntity:
    case EcsOpString:
    case EcsOpOpaque:
        len -= 2;

        char *result = ecs_os_malloc(len + 1);
        ecs_os_memcpy(result, value + 1, len);
        result[len] = '\0';

        if (ecs_meta_set_string(cursor, result)) {
            ecs_os_free(result);
            return -1;
        }

        ecs_os_free(result);
        break;
    }

    return 0;
}

int ecs_meta_set_entity(
    ecs_meta_cursor_t *cursor,
    ecs_entity_t value)
{
    ecs_meta_scope_t *scope = flecs_meta_cursor_get_scope(cursor);
    ecs_meta_type_op_t *op = flecs_meta_cursor_get_op(scope);
    void *ptr = flecs_meta_cursor_get_ptr(cursor->world, scope);

    switch(op->kind) {
    case EcsOpEntity:
        set_T(ecs_entity_t, ptr, value);
        break;
    case EcsOpOpaque: {
        const EcsOpaque *opaque = ecs_get(cursor->world, op->type, EcsOpaque);
        if (opaque && opaque->assign_entity) {
            opaque->assign_entity(ptr, (ecs_world_t*)cursor->world, value);
            break;
        }
    }
    /* fall through */
    default:
        flecs_meta_conversion_error(cursor, op, "entity");
        return -1;
    }

    return 0;
}

int ecs_meta_set_null(
    ecs_meta_cursor_t *cursor)
{
    ecs_meta_scope_t *scope = flecs_meta_cursor_get_scope(cursor);
    ecs_meta_type_op_t *op = flecs_meta_cursor_get_op(scope);
    void *ptr = flecs_meta_cursor_get_ptr(cursor->world, scope);
    switch (op->kind) {
    case EcsOpString:
        ecs_os_free(*(char**)ptr);
        set_T(ecs_string_t, ptr, NULL);
        break;
    case EcsOpOpaque: {
        const EcsOpaque *ot = ecs_get(cursor->world, op->type, EcsOpaque);
        if (ot && ot->assign_null) {
            ot->assign_null(ptr);
            break;
        }
    }
    /* fall through */
    default:
        flecs_meta_conversion_error(cursor, op, "null");
        return -1;
    }

    return 0;
}

bool ecs_meta_get_bool(
    const ecs_meta_cursor_t *cursor)
{
    ecs_meta_scope_t *scope = flecs_meta_cursor_get_scope(cursor);
    ecs_meta_type_op_t *op = flecs_meta_cursor_get_op(scope);
    void *ptr = flecs_meta_cursor_get_ptr(cursor->world, scope);
    switch(op->kind) {
    case EcsOpBool: return *(ecs_bool_t*)ptr;
    case EcsOpI8:   return *(ecs_i8_t*)ptr != 0;
    case EcsOpU8:   return *(ecs_u8_t*)ptr != 0;
    case EcsOpChar: return *(ecs_char_t*)ptr != 0;
    case EcsOpByte: return *(ecs_u8_t*)ptr != 0;
    case EcsOpI16:  return *(ecs_i16_t*)ptr != 0;
    case EcsOpU16:  return *(ecs_u16_t*)ptr != 0;
    case EcsOpI32:  return *(ecs_i32_t*)ptr != 0;
    case EcsOpU32:  return *(ecs_u32_t*)ptr != 0;
    case EcsOpI64:  return *(ecs_i64_t*)ptr != 0;
    case EcsOpU64:  return *(ecs_u64_t*)ptr != 0;
    case EcsOpIPtr: return *(ecs_iptr_t*)ptr != 0;
    case EcsOpUPtr: return *(ecs_uptr_t*)ptr != 0;
    case EcsOpF32:  return *(ecs_f32_t*)ptr != 0;
    case EcsOpF64:  return *(ecs_f64_t*)ptr != 0;
    case EcsOpString: return *(const char**)ptr != NULL;
    case EcsOpEnum: return *(ecs_i32_t*)ptr != 0;
    case EcsOpBitmask: return *(ecs_u32_t*)ptr != 0;
    case EcsOpEntity: return *(ecs_entity_t*)ptr != 0;
    default: ecs_throw(ECS_INVALID_PARAMETER,
                "invalid element for bool");
    }

error:
    return 0;
}

char ecs_meta_get_char(
    const ecs_meta_cursor_t *cursor)
{
    ecs_meta_scope_t *scope = flecs_meta_cursor_get_scope(cursor);
    ecs_meta_type_op_t *op = flecs_meta_cursor_get_op(scope);
    void *ptr = flecs_meta_cursor_get_ptr(cursor->world, scope);
    switch(op->kind) {
    case EcsOpChar: return *(ecs_char_t*)ptr != 0;
    default: ecs_throw(ECS_INVALID_PARAMETER,
                "invalid element for char");
    }

error:
    return 0;
}

int64_t ecs_meta_get_int(
    const ecs_meta_cursor_t *cursor)
{
    ecs_meta_scope_t *scope = flecs_meta_cursor_get_scope(cursor);
    ecs_meta_type_op_t *op = flecs_meta_cursor_get_op(scope);
    void *ptr = flecs_meta_cursor_get_ptr(cursor->world, scope);
    switch(op->kind) {
    case EcsOpBool: return *(ecs_bool_t*)ptr;
    case EcsOpI8:   return *(ecs_i8_t*)ptr;
    case EcsOpU8:   return *(ecs_u8_t*)ptr;
    case EcsOpChar: return *(ecs_char_t*)ptr;
    case EcsOpByte: return *(ecs_u8_t*)ptr;
    case EcsOpI16:  return *(ecs_i16_t*)ptr;
    case EcsOpU16:  return *(ecs_u16_t*)ptr;
    case EcsOpI32:  return *(ecs_i32_t*)ptr;
    case EcsOpU32:  return *(ecs_u32_t*)ptr;
    case EcsOpI64:  return *(ecs_i64_t*)ptr;
    case EcsOpU64:  return flecs_uto(int64_t, *(ecs_u64_t*)ptr);
    case EcsOpIPtr: return *(ecs_iptr_t*)ptr;
    case EcsOpUPtr: return flecs_uto(int64_t, *(ecs_uptr_t*)ptr);
    case EcsOpF32:  return (int64_t)*(ecs_f32_t*)ptr;
    case EcsOpF64:  return (int64_t)*(ecs_f64_t*)ptr;
    case EcsOpString: return atoi(*(const char**)ptr);
    case EcsOpEnum: return *(ecs_i32_t*)ptr;
    case EcsOpBitmask: return *(ecs_u32_t*)ptr;
    case EcsOpEntity:
        ecs_throw(ECS_INVALID_PARAMETER,
            "invalid conversion from entity to int");
        break;
    default: ecs_throw(ECS_INVALID_PARAMETER, "invalid element for int");
    }

error:
    return 0;
}

uint64_t ecs_meta_get_uint(
    const ecs_meta_cursor_t *cursor)
{
    ecs_meta_scope_t *scope = flecs_meta_cursor_get_scope(cursor);
    ecs_meta_type_op_t *op = flecs_meta_cursor_get_op(scope);
    void *ptr = flecs_meta_cursor_get_ptr(cursor->world, scope);
    switch(op->kind) {
    case EcsOpBool: return *(ecs_bool_t*)ptr;
    case EcsOpI8:   return flecs_ito(uint64_t, *(ecs_i8_t*)ptr);
    case EcsOpU8:   return *(ecs_u8_t*)ptr;
    case EcsOpChar: return flecs_ito(uint64_t, *(ecs_char_t*)ptr);
    case EcsOpByte: return flecs_ito(uint64_t, *(ecs_u8_t*)ptr);
    case EcsOpI16:  return flecs_ito(uint64_t, *(ecs_i16_t*)ptr);
    case EcsOpU16:  return *(ecs_u16_t*)ptr;
    case EcsOpI32:  return flecs_ito(uint64_t, *(ecs_i32_t*)ptr);
    case EcsOpU32:  return *(ecs_u32_t*)ptr;
    case EcsOpI64:  return flecs_ito(uint64_t, *(ecs_i64_t*)ptr);
    case EcsOpU64:  return *(ecs_u64_t*)ptr;
    case EcsOpIPtr: return flecs_ito(uint64_t, *(ecs_i64_t*)ptr);
    case EcsOpUPtr: return *(ecs_uptr_t*)ptr;
    case EcsOpF32:  return flecs_ito(uint64_t, *(ecs_f32_t*)ptr);
    case EcsOpF64:  return flecs_ito(uint64_t, *(ecs_f64_t*)ptr);
    case EcsOpString: return flecs_ito(uint64_t, atoi(*(const char**)ptr));
    case EcsOpEnum: return flecs_ito(uint64_t, *(ecs_i32_t*)ptr);
    case EcsOpBitmask: return *(ecs_u32_t*)ptr;
    case EcsOpEntity: return *(ecs_entity_t*)ptr;
    default: ecs_throw(ECS_INVALID_PARAMETER, "invalid element for uint");
    }
error:
    return 0;
}

static
double flecs_meta_to_float(
    ecs_meta_type_op_kind_t kind,
    const void *ptr)
{
    switch(kind) {
    case EcsOpBool: return *(ecs_bool_t*)ptr;
    case EcsOpI8:   return *(ecs_i8_t*)ptr;
    case EcsOpU8:   return *(ecs_u8_t*)ptr;
    case EcsOpChar: return *(ecs_char_t*)ptr;
    case EcsOpByte: return *(ecs_u8_t*)ptr;
    case EcsOpI16:  return *(ecs_i16_t*)ptr;
    case EcsOpU16:  return *(ecs_u16_t*)ptr;
    case EcsOpI32:  return *(ecs_i32_t*)ptr;
    case EcsOpU32:  return *(ecs_u32_t*)ptr;
    case EcsOpI64:  return (double)*(ecs_i64_t*)ptr;
    case EcsOpU64:  return (double)*(ecs_u64_t*)ptr;
    case EcsOpIPtr: return (double)*(ecs_iptr_t*)ptr;
    case EcsOpUPtr: return (double)*(ecs_uptr_t*)ptr;
    case EcsOpF32:  return (double)*(ecs_f32_t*)ptr;
    case EcsOpF64:  return *(ecs_f64_t*)ptr;
    case EcsOpString: return atof(*(const char**)ptr);
    case EcsOpEnum: return *(ecs_i32_t*)ptr;
    case EcsOpBitmask: return *(ecs_u32_t*)ptr;
    case EcsOpEntity:
        ecs_throw(ECS_INVALID_PARAMETER,
            "invalid conversion from entity to float");
        break;
    default: ecs_throw(ECS_INVALID_PARAMETER, "invalid element for float");
    }
error:
    return 0;
}

double ecs_meta_get_float(
    const ecs_meta_cursor_t *cursor)
{
    ecs_meta_scope_t *scope = flecs_meta_cursor_get_scope(cursor);
    ecs_meta_type_op_t *op = flecs_meta_cursor_get_op(scope);
    void *ptr = flecs_meta_cursor_get_ptr(cursor->world, scope);
    return flecs_meta_to_float(op->kind, ptr);
}

const char* ecs_meta_get_string(
    const ecs_meta_cursor_t *cursor)
{
    ecs_meta_scope_t *scope = flecs_meta_cursor_get_scope(cursor);
    ecs_meta_type_op_t *op = flecs_meta_cursor_get_op(scope);
    void *ptr = flecs_meta_cursor_get_ptr(cursor->world, scope);
    switch(op->kind) {
    case EcsOpString: return *(const char**)ptr;
    default: ecs_throw(ECS_INVALID_PARAMETER, "invalid element for string");
    }
error:
    return 0;
}

ecs_entity_t ecs_meta_get_entity(
    const ecs_meta_cursor_t *cursor)
{
    ecs_meta_scope_t *scope = flecs_meta_cursor_get_scope(cursor);
    ecs_meta_type_op_t *op = flecs_meta_cursor_get_op(scope);
    void *ptr = flecs_meta_cursor_get_ptr(cursor->world, scope);
    switch(op->kind) {
    case EcsOpEntity: return *(ecs_entity_t*)ptr;
    default: ecs_throw(ECS_INVALID_PARAMETER, "invalid element for entity");
    }
error:
    return 0;
}

double ecs_meta_ptr_to_float(
    ecs_primitive_kind_t type_kind,
    const void *ptr)
{
    ecs_meta_type_op_kind_t kind = flecs_meta_primitive_to_op_kind(type_kind);
    return flecs_meta_to_float(kind, ptr);
}

#endif

/**
 * @file expr/serialize.c
 * @brief Serialize (component) values to flecs string format.
 */


#ifdef FLECS_EXPR

static
int flecs_expr_ser_type(
    const ecs_world_t *world,
    const ecs_vec_t *ser, 
    const void *base, 
    ecs_strbuf_t *str,
    bool is_expr);

static
int flecs_expr_ser_type_ops(
    const ecs_world_t *world,
    ecs_meta_type_op_t *ops,
    int32_t op_count,
    const void *base, 
    ecs_strbuf_t *str,
    int32_t in_array,
    bool is_expr);

static
int flecs_expr_ser_type_op(
    const ecs_world_t *world,
    ecs_meta_type_op_t *op, 
    const void *base,
    ecs_strbuf_t *str,
    bool is_expr);

static
ecs_primitive_kind_t flecs_expr_op_to_primitive_kind(ecs_meta_type_op_kind_t kind) {
    return kind - EcsOpPrimitive;
}

/* Serialize a primitive value */
static
int flecs_expr_ser_primitive(
    const ecs_world_t *world,
    ecs_primitive_kind_t kind,
    const void *base, 
    ecs_strbuf_t *str,
    bool is_expr) 
{
    switch(kind) {
    case EcsBool:
        if (*(bool*)base) {
            ecs_strbuf_appendlit(str, "true");
        } else {
            ecs_strbuf_appendlit(str, "false");
        }
        break;
    case EcsChar: {
        char chbuf[3];
        char ch = *(char*)base;
        if (ch) {
            ecs_chresc(chbuf, *(char*)base, '"');
            if (is_expr) ecs_strbuf_appendch(str, '"');
            ecs_strbuf_appendstr(str, chbuf);
            if (is_expr) ecs_strbuf_appendch(str, '"');
        } else {
            ecs_strbuf_appendch(str, '0');
        }
        break;
    }
    case EcsByte:
        ecs_strbuf_appendint(str, flecs_uto(int64_t, *(uint8_t*)base));
        break;
    case EcsU8:
        ecs_strbuf_appendint(str, flecs_uto(int64_t, *(uint8_t*)base));
        break;
    case EcsU16:
        ecs_strbuf_appendint(str, flecs_uto(int64_t, *(uint16_t*)base));
        break;
    case EcsU32:
        ecs_strbuf_appendint(str, flecs_uto(int64_t, *(uint32_t*)base));
        break;
    case EcsU64:
        ecs_strbuf_append(str, "%llu", *(uint64_t*)base);
        break;
    case EcsI8:
        ecs_strbuf_appendint(str, flecs_ito(int64_t, *(int8_t*)base));
        break;
    case EcsI16:
        ecs_strbuf_appendint(str, flecs_ito(int64_t, *(int16_t*)base));
        break;
    case EcsI32:
        ecs_strbuf_appendint(str, flecs_ito(int64_t, *(int32_t*)base));
        break;
    case EcsI64:
        ecs_strbuf_appendint(str, *(int64_t*)base);
        break;
    case EcsF32:
        ecs_strbuf_appendflt(str, (double)*(float*)base, 0);
        break;
    case EcsF64:
        ecs_strbuf_appendflt(str, *(double*)base, 0);
        break;
    case EcsIPtr:
        ecs_strbuf_appendint(str, flecs_ito(int64_t, *(intptr_t*)base));
        break;
    case EcsUPtr:
        ecs_strbuf_append(str, "%u", *(uintptr_t*)base);
        break;
    case EcsString: {
        char *value = *(char**)base;
        if (value) {
            if (!is_expr) {
                ecs_strbuf_appendstr(str, value);
            } else {
                ecs_size_t length = ecs_stresc(NULL, 0, '"', value);
                if (length == ecs_os_strlen(value)) {
                    ecs_strbuf_appendch(str, '"');
                    ecs_strbuf_appendstrn(str, value, length);
                    ecs_strbuf_appendch(str, '"');
                } else {
                    char *out = ecs_os_malloc(length + 3);
                    ecs_stresc(out + 1, length, '"', value);
                    out[0] = '"';
                    out[length + 1] = '"';
                    out[length + 2] = '\0';
                    ecs_strbuf_appendstr_zerocpy(str, out);
                }
            }
        } else {
            ecs_strbuf_appendlit(str, "null");
        }
        break;
    }
    case EcsEntity: {
        ecs_entity_t e = *(ecs_entity_t*)base;
        if (!e) {
            ecs_strbuf_appendch(str, '0');
        } else {
            ecs_get_path_w_sep_buf(world, 0, e, ".", NULL, str);
        }
        break;
    }
    default:
        ecs_err("invalid primitive kind");
        return -1;
    }

    return 0;
}

/* Serialize enumeration */
static
int flecs_expr_ser_enum(
    const ecs_world_t *world,
    ecs_meta_type_op_t *op, 
    const void *base, 
    ecs_strbuf_t *str) 
{
    const EcsEnum *enum_type = ecs_get(world, op->type, EcsEnum);
    ecs_check(enum_type != NULL, ECS_INVALID_PARAMETER, NULL);

    int32_t val = *(int32_t*)base;
    
    /* Enumeration constants are stored in a map that is keyed on the
     * enumeration value. */
    ecs_enum_constant_t *c = ecs_map_get_deref(&enum_type->constants, 
        ecs_enum_constant_t, (ecs_map_key_t)val);
    if (!c) {
        char *path = ecs_get_fullpath(world, op->type);
        ecs_err("value %d is not valid for enum type '%s'", val, path);
        ecs_os_free(path);
        goto error;
    }

    ecs_strbuf_appendstr(str, ecs_get_name(world, c->constant));

    return 0;
error:
    return -1;
}

/* Serialize bitmask */
static
int flecs_expr_ser_bitmask(
    const ecs_world_t *world,
    ecs_meta_type_op_t *op, 
    const void *ptr, 
    ecs_strbuf_t *str) 
{
    const EcsBitmask *bitmask_type = ecs_get(world, op->type, EcsBitmask);
    ecs_check(bitmask_type != NULL, ECS_INVALID_PARAMETER, NULL);
    uint32_t value = *(uint32_t*)ptr;

    ecs_strbuf_list_push(str, "", "|");

    /* Multiple flags can be set at a given time. Iterate through all the flags
     * and append the ones that are set. */
    ecs_map_iter_t it = ecs_map_iter(&bitmask_type->constants);
    int count = 0;
    while (ecs_map_next(&it)) {
        ecs_bitmask_constant_t *c = ecs_map_ptr(&it);
        ecs_map_key_t key = ecs_map_key(&it);
        if ((value & key) == key) {
            ecs_strbuf_list_appendstr(str, ecs_get_name(world, c->constant));
            count ++;
            value -= (uint32_t)key;
        }
    }

    if (value != 0) {
        /* All bits must have been matched by a constant */
        char *path = ecs_get_fullpath(world, op->type);
        ecs_err(
            "value for bitmask %s contains bits (%u) that cannot be mapped to constant", 
            path, value);
        ecs_os_free(path);
        goto error;
    }

    if (!count) {
        ecs_strbuf_list_appendstr(str, "0");
    }

    ecs_strbuf_list_pop(str, "");

    return 0;
error:
    return -1;
}

/* Serialize elements of a contiguous array */
static
int expr_ser_elements(
    const ecs_world_t *world,
    ecs_meta_type_op_t *ops, 
    int32_t op_count,
    const void *base, 
    int32_t elem_count, 
    int32_t elem_size,
    ecs_strbuf_t *str,
    bool is_array)
{
    ecs_strbuf_list_push(str, "[", ", ");

    const void *ptr = base;

    int i;
    for (i = 0; i < elem_count; i ++) {
        ecs_strbuf_list_next(str);
        if (flecs_expr_ser_type_ops(
            world, ops, op_count, ptr, str, is_array, true)) 
        {
            return -1;
        }
        ptr = ECS_OFFSET(ptr, elem_size);
    }

    ecs_strbuf_list_pop(str, "]");

    return 0;
}

static
int expr_ser_type_elements(
    const ecs_world_t *world,
    ecs_entity_t type, 
    const void *base, 
    int32_t elem_count, 
    ecs_strbuf_t *str,
    bool is_array)
{
    const EcsMetaTypeSerialized *ser = ecs_get(
        world, type, EcsMetaTypeSerialized);
    ecs_assert(ser != NULL, ECS_INTERNAL_ERROR, NULL);

    const EcsComponent *comp = ecs_get(world, type, EcsComponent);
    ecs_assert(comp != NULL, ECS_INTERNAL_ERROR, NULL);

    ecs_meta_type_op_t *ops = ecs_vec_first_t(&ser->ops, ecs_meta_type_op_t);
    int32_t op_count = ecs_vec_count(&ser->ops);
    return expr_ser_elements(
        world, ops, op_count, base, elem_count, comp->size, str, is_array);
}

/* Serialize array */
static
int expr_ser_array(
    const ecs_world_t *world,
    ecs_meta_type_op_t *op, 
    const void *ptr, 
    ecs_strbuf_t *str) 
{
    const EcsArray *a = ecs_get(world, op->type, EcsArray);
    ecs_assert(a != NULL, ECS_INTERNAL_ERROR, NULL);

    return expr_ser_type_elements(
        world, a->type, ptr, a->count, str, true);
}

/* Serialize vector */
static
int expr_ser_vector(
    const ecs_world_t *world,
    ecs_meta_type_op_t *op, 
    const void *base, 
    ecs_strbuf_t *str) 
{
    const ecs_vec_t *value = base;
    const EcsVector *v = ecs_get(world, op->type, EcsVector);
    ecs_assert(v != NULL, ECS_INTERNAL_ERROR, NULL);

    int32_t count = ecs_vec_count(value);
    void *array = ecs_vec_first(value);

    /* Serialize contiguous buffer of vector */
    return expr_ser_type_elements(world, v->type, array, count, str, false);
}

/* Forward serialization to the different type kinds */
static
int flecs_expr_ser_type_op(
    const ecs_world_t *world,
    ecs_meta_type_op_t *op, 
    const void *ptr,
    ecs_strbuf_t *str,
    bool is_expr)
{
    switch(op->kind) {
    case EcsOpPush:
    case EcsOpPop:
        /* Should not be parsed as single op */
        ecs_throw(ECS_INVALID_PARAMETER, NULL);
        break;
    case EcsOpEnum:
        if (flecs_expr_ser_enum(world, op, ECS_OFFSET(ptr, op->offset), str)) {
            goto error;
        }
        break;
    case EcsOpBitmask:
        if (flecs_expr_ser_bitmask(world, op, ECS_OFFSET(ptr, op->offset), str)) {
            goto error;
        }
        break;
    case EcsOpArray:
        if (expr_ser_array(world, op, ECS_OFFSET(ptr, op->offset), str)) {
            goto error;
        }
        break;
    case EcsOpVector:
        if (expr_ser_vector(world, op, ECS_OFFSET(ptr, op->offset), str)) {
            goto error;
        }
        break;
    default:
        if (flecs_expr_ser_primitive(world, flecs_expr_op_to_primitive_kind(op->kind), 
            ECS_OFFSET(ptr, op->offset), str, is_expr))
        {
            /* Unknown operation */
            ecs_err("unknown serializer operation kind (%d)", op->kind);
            goto error;
        }
        break;
    }

    return 0;
error:
    return -1;
}

/* Iterate over a slice of the type ops array */
static
int flecs_expr_ser_type_ops(
    const ecs_world_t *world,
    ecs_meta_type_op_t *ops,
    int32_t op_count,
    const void *base,
    ecs_strbuf_t *str,
    int32_t in_array,
    bool is_expr) 
{
    for (int i = 0; i < op_count; i ++) {
        ecs_meta_type_op_t *op = &ops[i];

        if (in_array <= 0) {
            if (op->name) {
                ecs_strbuf_list_next(str);
                ecs_strbuf_append(str, "%s: ", op->name);
            }

            int32_t elem_count = op->count;
            if (elem_count > 1) {
                /* Serialize inline array */
                if (expr_ser_elements(world, op, op->op_count, base,
                    elem_count, op->size, str, true))
                {
                    return -1;
                }

                i += op->op_count - 1;
                continue;
            }
        }

        switch(op->kind) {
        case EcsOpPush:
            ecs_strbuf_list_push(str, "{", ", ");
            in_array --;
            break;
        case EcsOpPop:
            ecs_strbuf_list_pop(str, "}");
            in_array ++;
            break;
        default:
            if (flecs_expr_ser_type_op(world, op, base, str, is_expr)) {
                goto error;
            }
            break;
        }
    }

    return 0;
error:
    return -1;
}

/* Iterate over the type ops of a type */
static
int flecs_expr_ser_type(
    const ecs_world_t *world,
    const ecs_vec_t *v_ops,
    const void *base, 
    ecs_strbuf_t *str,
    bool is_expr) 
{
    ecs_meta_type_op_t *ops = ecs_vec_first_t(v_ops, ecs_meta_type_op_t);
    int32_t count = ecs_vec_count(v_ops);
    return flecs_expr_ser_type_ops(world, ops, count, base, str, 0, is_expr);
}

int ecs_ptr_to_expr_buf(
    const ecs_world_t *world,
    ecs_entity_t type,
    const void *ptr,
    ecs_strbuf_t *buf_out)
{
    const EcsMetaTypeSerialized *ser = ecs_get(
        world, type, EcsMetaTypeSerialized);
    if (ser == NULL) {
        char *path = ecs_get_fullpath(world, type);
        ecs_err("cannot serialize value for type '%s'", path);
        ecs_os_free(path);
        goto error;
    }

    if (flecs_expr_ser_type(world, &ser->ops, ptr, buf_out, true)) {
        goto error;
    }

    return 0;
error:
    return -1;
}

char* ecs_ptr_to_expr(
    const ecs_world_t *world, 
    ecs_entity_t type, 
    const void* ptr)
{
    ecs_strbuf_t str = ECS_STRBUF_INIT;

    if (ecs_ptr_to_expr_buf(world, type, ptr, &str) != 0) {
        ecs_strbuf_reset(&str);
        return NULL;
    }

    return ecs_strbuf_get(&str);
}

int ecs_ptr_to_str_buf(
    const ecs_world_t *world,
    ecs_entity_t type,
    const void *ptr,
    ecs_strbuf_t *buf_out)
{
    const EcsMetaTypeSerialized *ser = ecs_get(
        world, type, EcsMetaTypeSerialized);
    if (ser == NULL) {
        char *path = ecs_get_fullpath(world, type);
        ecs_err("cannot serialize value for type '%s'", path);
        ecs_os_free(path);
        goto error;
    }

    if (flecs_expr_ser_type(world, &ser->ops, ptr, buf_out, false)) {
        goto error;
    }

    return 0;
error:
    return -1;
}

char* ecs_ptr_to_str(
    const ecs_world_t *world, 
    ecs_entity_t type, 
    const void* ptr)
{
    ecs_strbuf_t str = ECS_STRBUF_INIT;

    if (ecs_ptr_to_str_buf(world, type, ptr, &str) != 0) {
        ecs_strbuf_reset(&str);
        return NULL;
    }

    return ecs_strbuf_get(&str);
}

int ecs_primitive_to_expr_buf(
    const ecs_world_t *world,
    ecs_primitive_kind_t kind,
    const void *base, 
    ecs_strbuf_t *str)
{
    return flecs_expr_ser_primitive(world, kind, base, str, true);
}

#endif

/**
 * @file expr/vars.c
 * @brief Utilities for variable substitution in flecs string expressions.
 */


#ifdef FLECS_EXPR

static
void flecs_expr_var_scope_init(
    ecs_world_t *world,
    ecs_expr_var_scope_t *scope,
    ecs_expr_var_scope_t *parent)
{
    flecs_name_index_init(&scope->var_index, &world->allocator);
    ecs_vec_init_t(&world->allocator, &scope->vars, ecs_expr_var_t, 0);
    scope->parent = parent;
}

static
void flecs_expr_var_scope_fini(
    ecs_world_t *world,
    ecs_expr_var_scope_t *scope)
{
    ecs_vec_t *vars = &scope->vars;
    int32_t i, count = vars->count;
    for (i = 0; i < count; i++) {
        ecs_expr_var_t *var = ecs_vec_get_t(vars, ecs_expr_var_t, i);
        if (var->owned) {
            ecs_value_free(world, var->value.type, var->value.ptr);
        }
        flecs_strfree(&world->allocator, var->name);
    }

    ecs_vec_fini_t(&world->allocator, &scope->vars, ecs_expr_var_t);
    flecs_name_index_fini(&scope->var_index);
}

void ecs_vars_init(
    ecs_world_t *world,
    ecs_vars_t *vars)
{
    flecs_expr_var_scope_init(world, &vars->root, NULL);
    vars->world = world;
    vars->cur = &vars->root;
}

void ecs_vars_fini(
    ecs_vars_t *vars)
{
    ecs_expr_var_scope_t *cur = vars->cur, *next;
    do {
        next = cur->parent;
        flecs_expr_var_scope_fini(vars->world, cur);
        if (cur != &vars->root) {
            flecs_free_t(&vars->world->allocator, ecs_expr_var_scope_t, cur);
        } else {
            break;
        }
    } while ((cur = next));
}

void ecs_vars_push(
    ecs_vars_t *vars)
{
    ecs_expr_var_scope_t *scope = flecs_calloc_t(&vars->world->allocator, 
        ecs_expr_var_scope_t);
    flecs_expr_var_scope_init(vars->world, scope, vars->cur);
    vars->cur = scope;
}

int ecs_vars_pop(
    ecs_vars_t *vars)
{
    ecs_expr_var_scope_t *scope = vars->cur;
    ecs_check(scope != &vars->root, ECS_INVALID_OPERATION, NULL);
    vars->cur = scope->parent;
    flecs_expr_var_scope_fini(vars->world, scope);
    flecs_free_t(&vars->world->allocator, ecs_expr_var_scope_t, scope);
    return 0;
error:
    return 1;
}

ecs_expr_var_t* ecs_vars_declare(
    ecs_vars_t *vars,
    const char *name,
    ecs_entity_t type)
{
    ecs_assert(vars != NULL, ECS_INVALID_PARAMETER, NULL);
    ecs_assert(name != NULL, ECS_INVALID_PARAMETER, NULL);
    ecs_assert(type != 0, ECS_INVALID_PARAMETER, NULL);
    ecs_expr_var_scope_t *scope = vars->cur;
    ecs_hashmap_t *var_index = &scope->var_index;

    if (flecs_name_index_find(var_index, name, 0, 0) != 0) {
        ecs_err("variable %s redeclared", name);
        goto error;
    }

    ecs_expr_var_t *var = ecs_vec_append_t(&vars->world->allocator, 
        &scope->vars, ecs_expr_var_t);
    
    var->value.ptr = ecs_value_new(vars->world, type);
    if (!var->value.ptr) {
        goto error;
    }
    var->value.type = type;
    var->name = flecs_strdup(&vars->world->allocator, name);
    var->owned = true;

    flecs_name_index_ensure(var_index, 
        flecs_ito(uint64_t, ecs_vec_count(&scope->vars)), var->name, 0, 0);
    return var;
error:
    return NULL;
}

ecs_expr_var_t* ecs_vars_declare_w_value(
    ecs_vars_t *vars,
    const char *name,
    ecs_value_t *value)
{
    ecs_assert(vars != NULL, ECS_INVALID_PARAMETER, NULL);
    ecs_assert(name != NULL, ECS_INVALID_PARAMETER, NULL);
    ecs_assert(value != NULL, ECS_INVALID_PARAMETER, NULL);
    ecs_expr_var_scope_t *scope = vars->cur;
    ecs_hashmap_t *var_index = &scope->var_index;

    if (flecs_name_index_find(var_index, name, 0, 0) != 0) {
        ecs_err("variable %s redeclared", name);
        ecs_value_free(vars->world, value->type, value->ptr);
        goto error;
    }

    ecs_expr_var_t *var = ecs_vec_append_t(&vars->world->allocator, 
        &scope->vars, ecs_expr_var_t);
    var->value = *value;
    var->name = flecs_strdup(&vars->world->allocator, name);
    var->owned = true;
    value->ptr = NULL; /* Take ownership, prevent double free */

    flecs_name_index_ensure(var_index, 
        flecs_ito(uint64_t, ecs_vec_count(&scope->vars)), var->name, 0, 0);
    return var;
error:
    return NULL;
}

static
ecs_expr_var_t* flecs_vars_scope_lookup(
    ecs_expr_var_scope_t *scope,
    const char *name)
{
    uint64_t var_id = flecs_name_index_find(&scope->var_index, name, 0, 0);
    if (var_id == 0) {
        if (scope->parent) {
            return flecs_vars_scope_lookup(scope->parent, name);
        }
        return NULL;
    }

    return ecs_vec_get_t(&scope->vars, ecs_expr_var_t, 
        flecs_uto(int32_t, var_id - 1));
}

ecs_expr_var_t* ecs_vars_lookup(
    const ecs_vars_t *vars,
    const char *name)
{
    return flecs_vars_scope_lookup(vars->cur, name);
}

#endif

/**
 * @file expr/strutil.c
 * @brief String parsing utilities.
 */


#ifdef FLECS_EXPR

#include <ctype.h>

char* ecs_chresc(
    char *out, 
    char in, 
    char delimiter) 
{
    char *bptr = out;
    switch(in) {
    case '\a':
        *bptr++ = '\\';
        *bptr = 'a';
        break;
    case '\b':
        *bptr++ = '\\';
        *bptr = 'b';
        break;
    case '\f':
        *bptr++ = '\\';
        *bptr = 'f';
        break;
    case '\n':
        *bptr++ = '\\';
        *bptr = 'n';
        break;
    case '\r':
        *bptr++ = '\\';
        *bptr = 'r';
        break;
    case '\t':
        *bptr++ = '\\';
        *bptr = 't';
        break;
    case '\v':
        *bptr++ = '\\';
        *bptr = 'v';
        break;
    case '\\':
        *bptr++ = '\\';
        *bptr = '\\';
        break;
    default:
        if (in == delimiter) {
            *bptr++ = '\\';
            *bptr = delimiter;
        } else {
            *bptr = in;
        }
        break;
    }

    *(++bptr) = '\0';

    return bptr;
}

const char* ecs_chrparse(
    const char *in, 
    char *out) 
{
    const char *result = in + 1;
    char ch;

    if (in[0] == '\\') {
        result ++;

        switch(in[1]) {
        case 'a':
            ch = '\a';
            break;
        case 'b':
            ch = '\b';
            break;
        case 'f':
            ch = '\f';
            break;
        case 'n':
            ch = '\n';
            break;
        case 'r':
            ch = '\r';
            break;
        case 't':
            ch = '\t';
            break;
        case 'v':
            ch = '\v';
            break;
        case '\\':
            ch = '\\';
            break;
        case '"':
            ch = '"';
            break;
        case '0':
            ch = '\0';
            break;
        case ' ':
            ch = ' ';
            break;
        case '$':
            ch = '$';
            break;
        default:
            goto error;
        }
    } else {
        ch = in[0];
    }

    if (out) {
        *out = ch;
    }

    return result;
error:
    return NULL;
}

ecs_size_t ecs_stresc(
    char *out, 
    ecs_size_t n, 
    char delimiter, 
    const char *in) 
{
    const char *ptr = in;
    char ch, *bptr = out, buff[3];
    ecs_size_t written = 0;
    while ((ch = *ptr++)) {
        if ((written += (ecs_size_t)(ecs_chresc(
            buff, ch, delimiter) - buff)) <= n) 
        {
            /* If size != 0, an out buffer must be provided. */
            ecs_check(out != NULL, ECS_INVALID_PARAMETER, NULL);
            *bptr++ = buff[0];
            if ((ch = buff[1])) {
                *bptr = ch;
                bptr++;
            }
        }
    }

    if (bptr) {
        while (written < n) {
            *bptr = '\0';
            bptr++;
            written++;
        }
    }
    return written;
error:
    return 0;
}

char* ecs_astresc(
    char delimiter, 
    const char *in)
{
    if (!in) {
        return NULL;
    }

    ecs_size_t len = ecs_stresc(NULL, 0, delimiter, in);
    char *out = ecs_os_malloc_n(char, len + 1);
    ecs_stresc(out, len, delimiter, in);
    out[len] = '\0';
    return out;
}

static
const char* flecs_parse_var_name(
    const char *ptr,
    char *token_out)
{
    char ch, *bptr = token_out;

    while ((ch = *ptr)) {
        if (bptr - token_out > ECS_MAX_TOKEN_SIZE) {
            goto error;
        }

        if (isalpha(ch) || isdigit(ch) || ch == '_') {
            *bptr = ch;
            bptr ++;
            ptr ++;
        } else {
            break;
        }
    }

    if (bptr == token_out) {
        goto error;
    }

    *bptr = '\0';

    return ptr;
error:
    return NULL;
}

static
const char* flecs_parse_interpolated_str(
    const char *ptr,
    char *token_out)
{
    char ch, *bptr = token_out;

    while ((ch = *ptr)) {
        if (bptr - token_out > ECS_MAX_TOKEN_SIZE) {
            goto error;
        }

        if (ch == '\\') {
            if (ptr[1] == '}') {
                *bptr = '}';
                bptr ++;
                ptr += 2;
                continue;
            }
        }

        if (ch != '}') {
            *bptr = ch;
            bptr ++;
            ptr ++;
        } else {
            ptr ++;
            break;
        }
    }

    if (bptr == token_out) {
        goto error;
    }

    *bptr = '\0';

    return ptr;
error:
    return NULL;
}

char* ecs_interpolate_string(
    ecs_world_t *world,
    const char *str,
    const ecs_vars_t *vars)
{
    char token[ECS_MAX_TOKEN_SIZE];
    ecs_strbuf_t result = ECS_STRBUF_INIT;
    const char *ptr;
    char ch;

    for(ptr = str; (ch = *ptr); ptr++) {
        if (ch == '\\') {
            ptr ++;
            if (ptr[0] == '$') {
                ecs_strbuf_appendch(&result, '$');
                continue;
            }
            if (ptr[0] == '\\') {
                ecs_strbuf_appendch(&result, '\\');
                continue;
            }
            if (ptr[0] == '{') {
                ecs_strbuf_appendch(&result, '{');
                continue;
            }
            if (ptr[0] == '}') {
                ecs_strbuf_appendch(&result, '}');
                continue;
            }
            ptr --;
        }

        if (ch == '$') {
            ptr = flecs_parse_var_name(ptr + 1, token);
            if (!ptr) {
                ecs_parser_error(NULL, str, ptr - str, 
                    "invalid variable name '%s'", ptr);
                goto error;
            }

            ecs_expr_var_t *var = ecs_vars_lookup(vars, token);
            if (!var) {
                ecs_parser_error(NULL, str, ptr - str, 
                    "unresolved variable '%s'", token);
                goto error;
            }

            if (ecs_ptr_to_str_buf(
                world, var->value.type, var->value.ptr, &result)) 
            {
                goto error;
            }

            ptr --;
        } else if (ch == '{') {
            ptr = flecs_parse_interpolated_str(ptr + 1, token);
            if (!ptr) {
                ecs_parser_error(NULL, str, ptr - str, 
                    "invalid interpolated expression");
                goto error;
            }

            ecs_parse_expr_desc_t expr_desc = { .vars = (ecs_vars_t*)vars };
            ecs_value_t expr_result = {0};
            if (!ecs_parse_expr(world, token, &expr_result, &expr_desc)) {
                goto error;
            }

            if (ecs_ptr_to_str_buf(
                world, expr_result.type, expr_result.ptr, &result)) 
            {
                goto error;
            }

            ecs_value_free(world, expr_result.type, expr_result.ptr);

            ptr --;
        } else {
            ecs_strbuf_appendch(&result, ch);
        }
    }

    return ecs_strbuf_get(&result);
error:
    return NULL;
}

void ecs_iter_to_vars(
    const ecs_iter_t *it,
    ecs_vars_t *vars,
    int offset)
{
    ecs_check(vars != NULL, ECS_INVALID_PARAMETER, NULL);
    ecs_check(it != NULL, ECS_INVALID_PARAMETER, NULL);
    ecs_check(!offset || offset < it->count, ECS_INVALID_PARAMETER, NULL);

    /* Set variable for $this */
    if (it->count) {
        ecs_expr_var_t *var = ecs_vars_lookup(vars, "this");
        if (!var) {
            ecs_value_t v = { 
                .ptr = &it->entities[offset], 
                .type = ecs_id(ecs_entity_t) 
            };
            var = ecs_vars_declare_w_value(vars, "this", &v);
            var->owned = false;
        } else {
            var->value.ptr = &it->entities[offset];
        }
    }

    /* Set variables for fields */
    {
        int32_t i, field_count = it->field_count;
        for (i = 0; i < field_count; i ++) {
            ecs_size_t size = it->sizes[i];
            if (!size) {
                continue;
            }

            void *ptr = it->ptrs[i];
            if (!ptr) {
                continue;
            }

            ptr = ECS_OFFSET(ptr, offset * size);

            char name[16];
            ecs_os_sprintf(name, "%d", i + 1);
            ecs_expr_var_t *var = ecs_vars_lookup(vars, name);
            if (!var) {
                ecs_value_t v = { .ptr = ptr, .type = it->ids[i] };
                var = ecs_vars_declare_w_value(vars, name, &v);
                var->owned = false;
            } else {
                ecs_check(var->value.type == it->ids[i], 
                    ECS_INVALID_PARAMETER, NULL);
                var->value.ptr = ptr;
            }
        }
    }

    /* Set variables for query variables */
    {
        int32_t i, var_count = it->variable_count;
        for (i = 1 /* skip this variable */ ; i < var_count; i ++) {
            ecs_entity_t *e_ptr = NULL;
            ecs_var_t *query_var = &it->variables[i];
            if (query_var->entity) {
                e_ptr = &query_var->entity;
            } else {
                ecs_table_range_t *range = &query_var->range;
                if (range->count == 1) {
                    ecs_entity_t *entities = range->table->data.entities.array;
                    e_ptr = &entities[range->offset];
                }
            }
            if (!e_ptr) {
                continue;
            }

            ecs_expr_var_t *var = ecs_vars_lookup(vars, it->variable_names[i]);
            if (!var) {
                ecs_value_t v = { .ptr = e_ptr, .type = ecs_id(ecs_entity_t) };
                var = ecs_vars_declare_w_value(vars, it->variable_names[i], &v);
                var->owned = false;
            } else {
                ecs_check(var->value.type == ecs_id(ecs_entity_t), 
                    ECS_INVALID_PARAMETER, NULL);
                var->value.ptr = e_ptr;
            }
        }
    }

error:
    return;
}

#endif

/**
 * @file expr/deserialize.c
 * @brief Deserialize flecs string format into (component) values.
 */

#include <ctype.h>

#ifdef FLECS_EXPR

/* String deserializer for values & simple expressions */

/* Order in enumeration is important, as it is used for precedence */
typedef enum ecs_expr_oper_t {
    EcsExprOperUnknown,
    EcsLeftParen,
    EcsCondAnd,
    EcsCondOr,
    EcsCondEq,
    EcsCondNeq,
    EcsCondGt,
    EcsCondGtEq,
    EcsCondLt,
    EcsCondLtEq,
    EcsShiftLeft,
    EcsShiftRight,
    EcsAdd,
    EcsSub,
    EcsMul,
    EcsDiv,
    EcsMin
} ecs_expr_oper_t;

/* Used to track temporary values */
#define EXPR_MAX_STACK_SIZE (256)

typedef struct ecs_expr_value_t {
    const ecs_type_info_t *ti;
    void *ptr;
} ecs_expr_value_t;

typedef struct ecs_value_stack_t {
    ecs_expr_value_t values[EXPR_MAX_STACK_SIZE];
    ecs_stack_cursor_t cursor;
    ecs_stack_t *stack;
    ecs_stage_t *stage;
    int32_t count;
} ecs_value_stack_t;

static
const char* flecs_parse_expr(
    ecs_world_t *world,
    ecs_value_stack_t *stack,
    const char *ptr,
    ecs_value_t *value,
    ecs_expr_oper_t op,
    const ecs_parse_expr_desc_t *desc);

static
void* flecs_expr_value_new(
    ecs_value_stack_t *stack,
    ecs_entity_t type)
{
    ecs_stage_t *stage = stack->stage;
    ecs_world_t *world = stage->world;
    ecs_id_record_t *idr = flecs_id_record_get(world, type);
    if (!idr) {
        return NULL;
    }

    const ecs_type_info_t *ti = idr->type_info;
    if (!ti) {
        return NULL;
    }

    ecs_assert(ti->size != 0, ECS_INTERNAL_ERROR, NULL);
    void *result = flecs_stack_alloc(stack->stack, ti->size, ti->alignment);
    if (ti->hooks.ctor) {
        ti->hooks.ctor(result, 1, ti);
    } else {
        ecs_os_memset(result, 0, ti->size);
    }
    if (ti->hooks.dtor) {
        /* Track values that have destructors */
        stack->values[stack->count].ti = ti;
        stack->values[stack->count].ptr = result;
        stack->count ++;
    }

    return result;
}

static
const char* flecs_str_to_expr_oper(
    const char *str,
    ecs_expr_oper_t *op)
{
    if (!ecs_os_strncmp(str, "+", 1)) {
        *op = EcsAdd;
        return str + 1;
    } else if (!ecs_os_strncmp(str, "-", 1)) {
        *op = EcsSub;
        return str + 1;
    } else if (!ecs_os_strncmp(str, "*", 1)) {
        *op = EcsMul;
        return str + 1;
    } else if (!ecs_os_strncmp(str, "/", 1)) {
        *op = EcsDiv;
        return str + 1;
    } else if (!ecs_os_strncmp(str, "&&", 2)) {
        *op = EcsCondAnd;
        return str + 2;
    } else if (!ecs_os_strncmp(str, "||", 2)) {
        *op = EcsCondOr;
        return str + 2;
    } else if (!ecs_os_strncmp(str, "==", 2)) {
        *op = EcsCondEq;
        return str + 2;
    } else if (!ecs_os_strncmp(str, "!=", 2)) {
        *op = EcsCondNeq;
        return str + 2;
    } else if (!ecs_os_strncmp(str, ">=", 2)) {
        *op = EcsCondGtEq;
        return str + 2;
    } else if (!ecs_os_strncmp(str, "<=", 2)) {
        *op = EcsCondLtEq;
        return str + 2;
    } else if (!ecs_os_strncmp(str, ">>", 2)) {
        *op = EcsShiftRight;
        return str + 2;
    } else if (!ecs_os_strncmp(str, "<<", 2)) {
        *op = EcsShiftLeft;
        return str + 2;
    } else if (!ecs_os_strncmp(str, ">", 1)) {
        *op = EcsCondGt;
        return str + 1;
    } else if (!ecs_os_strncmp(str, "<", 1)) {
        *op = EcsCondLt;
        return str + 1;
    }

    *op = EcsExprOperUnknown;
    return NULL;
}

const char *ecs_parse_expr_token(
    const char *name,
    const char *expr,
    const char *ptr,
    char *token)
{
    const char *start = ptr;
    char *token_ptr = token;

    if (ptr[0] == '/') {
        char ch;
        if (ptr[1] == '/') {
            // Single line comment
            for (ptr = &ptr[2]; (ch = ptr[0]) && (ch != '\n'); ptr ++) {}
            return ptr;
        } else if (ptr[1] == '*') {
            // Multi line comment
            for (ptr = &ptr[2]; (ch = ptr[0]); ptr ++) {
                if (ch == '*' && ptr[1] == '/') {
                    return ptr + 2;
                }
            }

            ecs_parser_error(name, expr, ptr - expr, 
                "missing */ for multiline comment");
            return NULL;
        }
    }

    ecs_expr_oper_t op;
    if (ptr[0] == '(') {
        token[0] = '(';
        token[1] = 0;
        return ptr + 1;
    } else if (ptr[0] != '-') {
        const char *tptr = flecs_str_to_expr_oper(ptr, &op);
        if (tptr) {
            ecs_os_strncpy(token, ptr, tptr - ptr);
            return tptr;
        }
    }

    while ((ptr = ecs_parse_token(name, expr, ptr, token_ptr, 0))) {
        if (ptr[0] == '|' && ptr[1] != '|') {
            token_ptr = &token_ptr[ptr - start];
            token_ptr[0] = '|';
            token_ptr[1] = '\0';
            token_ptr ++;
            ptr ++;
            start = ptr;
        } else {
            break;
        }
    }

    return ptr;
}

static
const char* flecs_parse_multiline_string(
    ecs_meta_cursor_t *cur,
    const char *name,
    const char *expr,
    const char *ptr)
{
    /* Multiline string */
    ecs_strbuf_t str = ECS_STRBUF_INIT;
    char ch;
    while ((ch = ptr[0]) && (ch != '`')) {
        if (ch == '\\' && ptr[1] == '`') {
            ch = '`';
            ptr ++;
        }
        ecs_strbuf_appendch(&str, ch);
        ptr ++;
    }
    
    if (ch != '`') {
        ecs_parser_error(name, expr, ptr - expr, 
            "missing '`' to close multiline string");
        goto error;
    }
    char *strval = ecs_strbuf_get(&str);
    if (ecs_meta_set_string(cur, strval) != 0) {
        goto error;
    }
    ecs_os_free(strval);

    return ptr + 1;
error:
    return NULL;
}

static
bool flecs_parse_is_float(
    const char *ptr)
{
    ecs_assert(isdigit(ptr[0]), ECS_INTERNAL_ERROR, NULL);
    char ch;
    while ((ch = (++ptr)[0])) {
        if (ch == '.' || ch == 'e') {
            return true;
        }
        if (!isdigit(ch)) {
            return false;
        }
    }
    return false;
}

/* Attempt to resolve variable dotexpression to value (foo.bar) */
static
ecs_value_t flecs_dotresolve_var(
    ecs_world_t *world,
    ecs_vars_t *vars,
    char *token)
{
    char *dot = strchr(token, '.');
    if (!dot) {
        return (ecs_value_t){0};
    }

    dot[0] = '\0';

    const ecs_expr_var_t *var = ecs_vars_lookup(vars, token);
    if (!var) {
        return (ecs_value_t){0};
    }

    ecs_meta_cursor_t cur = ecs_meta_cursor(
        world, var->value.type, var->value.ptr);
    ecs_meta_push(&cur);
    if (ecs_meta_dotmember(&cur, dot + 1) != 0) {
        return (ecs_value_t){0};
    }

    return (ecs_value_t){ 
        .ptr = ecs_meta_get_ptr(&cur),
        .type = ecs_meta_get_type(&cur)
    };
}

/* Determine the type of an expression from the first character(s). This allows
 * us to initialize a storage for a type if none was provided. */
static
ecs_entity_t flecs_parse_discover_type(
    ecs_world_t *world,
    const char *name,
    const char *expr,
    const char *ptr,
    ecs_entity_t input_type,
    const ecs_parse_expr_desc_t *desc)
{
    /* String literal */
    if (ptr[0] == '"' || ptr[0] == '`') {
        if (input_type == ecs_id(ecs_char_t)) {
            return input_type;
        }
        return ecs_id(ecs_string_t);
    }

    /* Negative number literal */
    if (ptr[0] == '-') {
        if (!isdigit(ptr[1])) {
            ecs_parser_error(name, expr, ptr - expr, "invalid literal");
            return 0;
        }
        if (flecs_parse_is_float(ptr + 1)) {
            return ecs_id(ecs_f64_t);
        } else {
            return ecs_id(ecs_i64_t);
        }
    }

    /* Positive number literal */
    if (isdigit(ptr[0])) {
        if (flecs_parse_is_float(ptr)) {
            return ecs_id(ecs_f64_t);
        } else {
            return ecs_id(ecs_u64_t);
        }
    }

    /* Variable */
    if (ptr[0] == '$') {
        if (!desc || !desc->vars) {
            ecs_parser_error(name, expr, ptr - expr, 
                "unresolved variable (no variable scope)");
            return 0;
        }
        char token[ECS_MAX_TOKEN_SIZE];
        if (ecs_parse_expr_token(name, expr, &ptr[1], token) == NULL) {
            return 0;
        }

        const ecs_expr_var_t *var = ecs_vars_lookup(desc->vars, token);
        if (!var) {
            ecs_value_t v = flecs_dotresolve_var(world, desc->vars, token);
            if (v.type) {
                return v.type;
            }

            ecs_parser_error(name, expr, ptr - expr, 
                "unresolved variable '%s'", token);
            return 0;
        }
        return var->value.type;
    }

    /* Boolean */
    if (ptr[0] == 't' && !ecs_os_strncmp(ptr, "true", 4)) {
        if (!isalpha(ptr[4]) && ptr[4] != '_') {
            return ecs_id(ecs_bool_t);
        }
    }
    if (ptr[0] == 'f' && !ecs_os_strncmp(ptr, "false", 5)) {
        if (!isalpha(ptr[5]) && ptr[5] != '_') {
            return ecs_id(ecs_bool_t);
        }
    }

    /* Entity identifier */
    if (isalpha(ptr[0])) {
        if (!input_type) { /* Identifier could also be enum/bitmask constant */
            return ecs_id(ecs_entity_t);
        }
    }

    /* If no default type was provided we can't automatically deduce the type of
     * composite/collection expressions. */
    if (!input_type) {
        if (ptr[0] == '{') {
            ecs_parser_error(name, expr, ptr - expr,
                "unknown type for composite literal");
            return 0;
        }

        if (ptr[0] == '[') {
            ecs_parser_error(name, expr, ptr - expr,
                "unknown type for collection literal");
            return 0;
        }

        ecs_parser_error(name, expr, ptr - expr, "invalid expression");
    }

    return input_type;
}

/* Normalize types to their largest representation.
 * Rather than taking the original type of a value, use the largest 
 * representation of the type so we don't have to worry about overflowing the
 * original type in the operation. */
static
ecs_entity_t flecs_largest_type(
    const EcsPrimitive *type)
{
    switch(type->kind) {
    case EcsBool:   return ecs_id(ecs_bool_t);
    case EcsChar:   return ecs_id(ecs_char_t);
    case EcsByte:   return ecs_id(ecs_u8_t);
    case EcsU8:     return ecs_id(ecs_u64_t);
    case EcsU16:    return ecs_id(ecs_u64_t);
    case EcsU32:    return ecs_id(ecs_u64_t);
    case EcsU64:    return ecs_id(ecs_u64_t);
    case EcsI8:     return ecs_id(ecs_i64_t);
    case EcsI16:    return ecs_id(ecs_i64_t);
    case EcsI32:    return ecs_id(ecs_i64_t);
    case EcsI64:    return ecs_id(ecs_i64_t);
    case EcsF32:    return ecs_id(ecs_f64_t);
    case EcsF64:    return ecs_id(ecs_f64_t);
    case EcsUPtr:   return ecs_id(ecs_u64_t);
    case EcsIPtr:   return ecs_id(ecs_i64_t);
    case EcsString: return ecs_id(ecs_string_t);
    case EcsEntity: return ecs_id(ecs_entity_t);
    default: ecs_abort(ECS_INTERNAL_ERROR, NULL);
    }
    return 0;
}

/** Test if a normalized type can promote to another type in an expression */
static
bool flecs_is_type_number(
    ecs_entity_t type)
{
    if      (type == ecs_id(ecs_bool_t)) return false;
    else if (type == ecs_id(ecs_char_t)) return false;
    else if (type == ecs_id(ecs_u8_t)) return false;
    else if (type == ecs_id(ecs_u64_t)) return true;
    else if (type == ecs_id(ecs_i64_t)) return true;
    else if (type == ecs_id(ecs_f64_t)) return true;
    else if (type == ecs_id(ecs_string_t)) return false;
    else if (type == ecs_id(ecs_entity_t)) return false;
    else return false;
}

static
bool flecs_oper_valid_for_type(
    ecs_entity_t type,
    ecs_expr_oper_t op)
{
    switch(op) {
    case EcsAdd:
    case EcsSub:
    case EcsMul:
    case EcsDiv:
        return flecs_is_type_number(type);
    case EcsCondEq:
    case EcsCondNeq:
    case EcsCondAnd:
    case EcsCondOr:
    case EcsCondGt:
    case EcsCondGtEq:
    case EcsCondLt:
    case EcsCondLtEq:
        return flecs_is_type_number(type) ||
            (type == ecs_id(ecs_bool_t)) ||
            (type == ecs_id(ecs_char_t)) ||
            (type == ecs_id(ecs_entity_t));
    case EcsShiftLeft:
    case EcsShiftRight:
        return (type == ecs_id(ecs_u64_t));
    default: 
        return false;
    }
}

/** Promote type to most expressive (f64 > i64 > u64) */
static
ecs_entity_t flecs_promote_type(
    ecs_entity_t type,
    ecs_entity_t promote_to)
{
    if (type == ecs_id(ecs_u64_t)) {
        return promote_to;
    }
    if (promote_to == ecs_id(ecs_u64_t)) {
        return type;
    }
    if (type == ecs_id(ecs_f64_t)) {
        return type;
    }
    if (promote_to == ecs_id(ecs_f64_t)) {
        return promote_to;
    }
    return ecs_id(ecs_i64_t);
}

static
int flecs_oper_precedence(
    ecs_expr_oper_t left,
    ecs_expr_oper_t right)
{
    return (left > right) - (left < right);
}

static
void flecs_value_cast(
    ecs_world_t *world,
    ecs_value_stack_t *stack,
    ecs_value_t *value,
    ecs_entity_t type)
{
    if (value->type == type) {
        return;
    }

    ecs_value_t result;
    result.type = type;
    result.ptr = flecs_expr_value_new(stack, type);

    if (value->ptr) {
        ecs_meta_cursor_t cur = ecs_meta_cursor(world, type, result.ptr);
        ecs_meta_set_value(&cur, value);
    }

    *value = result;
}

static
bool flecs_expr_op_is_equality(
    ecs_expr_oper_t op)
{
    switch(op) {
    case EcsCondEq:
    case EcsCondNeq:
    case EcsCondGt:
    case EcsCondGtEq:
    case EcsCondLt:
    case EcsCondLtEq:
        return true;
    default:
        return false;
    }
}

static
ecs_entity_t flecs_binary_expr_type(
    ecs_world_t *world,
    const char *name,
    const char *expr,
    const char *ptr,
    ecs_value_t *lvalue,
    ecs_value_t *rvalue,
    ecs_expr_oper_t op,
    ecs_entity_t *operand_type_out)
{
    ecs_entity_t result_type = 0, operand_type = 0;

    switch(op) {
    case EcsDiv: 
        /* Result type of a division is always a float */
        *operand_type_out = ecs_id(ecs_f64_t);
        return ecs_id(ecs_f64_t);
    case EcsCondAnd:
    case EcsCondOr:
        /* Result type of a condition operator is always a bool */
        *operand_type_out = ecs_id(ecs_bool_t);
        return ecs_id(ecs_bool_t);
    case EcsCondEq:
    case EcsCondNeq:
    case EcsCondGt:
    case EcsCondGtEq:
    case EcsCondLt:
    case EcsCondLtEq:
        /* Result type of equality operator is always bool, but operand types
         * should not be casted to bool */
        result_type = ecs_id(ecs_bool_t);
        break;
    default:
        break;
    }

    /* Result type for arithmetic operators is determined by operands */
    const EcsPrimitive *ltype_ptr = ecs_get(world, lvalue->type, EcsPrimitive);
    const EcsPrimitive *rtype_ptr = ecs_get(world, rvalue->type, EcsPrimitive);
    if (!ltype_ptr || !rtype_ptr) {
        char *lname = ecs_get_fullpath(world, lvalue->type);
        char *rname = ecs_get_fullpath(world, rvalue->type);
        ecs_parser_error(name, expr, ptr - expr, 
            "invalid non-primitive type in binary expression (%s, %s)",
                lname, rname);
        ecs_os_free(lname);
        ecs_os_free(rname);
        return 0;
    }

    ecs_entity_t ltype = flecs_largest_type(ltype_ptr);
    ecs_entity_t rtype = flecs_largest_type(rtype_ptr);
    if (ltype == rtype) {
        operand_type = ltype;
        goto done;
    }

    if (flecs_expr_op_is_equality(op)) {
        ecs_parser_error(name, expr, ptr - expr, 
            "mismatching types in equality expression");
        return 0;
    }

    if (!flecs_is_type_number(ltype) || !flecs_is_type_number(rtype)) {
        ecs_parser_error(name, expr, ptr - expr, 
            "incompatible types in binary expression");
        return 0;
    }

    operand_type = flecs_promote_type(ltype, rtype);

done:
    if (op == EcsSub && operand_type == ecs_id(ecs_u64_t)) {
        /* Result of subtracting two unsigned ints can be negative */
        operand_type = ecs_id(ecs_i64_t);
    }

    if (!result_type) {
        result_type = operand_type;
    }

    *operand_type_out = operand_type;
    return result_type;
}

/* Macro's to let the compiler do the operations & conversion work for us */

#define ECS_VALUE_GET(value, T) (*(T*)value->ptr)

#define ECS_BINARY_OP_T(left, right, result, op, R, T)\
    ECS_VALUE_GET(result, R) = ECS_VALUE_GET(left, T) op ECS_VALUE_GET(right, T)

#define ECS_BINARY_OP(left, right, result, op)\
    if (left->type == ecs_id(ecs_u64_t)) { \
        ECS_BINARY_OP_T(left, right, result, op, ecs_u64_t, ecs_u64_t);\
    } else if (left->type == ecs_id(ecs_i64_t)) { \
        ECS_BINARY_OP_T(left, right, result, op, ecs_i64_t, ecs_i64_t);\
    } else if (left->type == ecs_id(ecs_f64_t)) { \
        ECS_BINARY_OP_T(left, right, result, op, ecs_f64_t, ecs_f64_t);\
    } else {\
        ecs_abort(ECS_INTERNAL_ERROR, "unexpected type in binary expression");\
    }

#define ECS_BINARY_COND_OP(left, right, result, op)\
    if (left->type == ecs_id(ecs_u64_t)) { \
        ECS_BINARY_OP_T(left, right, result, op, ecs_bool_t, ecs_u64_t);\
    } else if (left->type == ecs_id(ecs_i64_t)) { \
        ECS_BINARY_OP_T(left, right, result, op, ecs_bool_t, ecs_i64_t);\
    } else if (left->type == ecs_id(ecs_f64_t)) { \
        ECS_BINARY_OP_T(left, right, result, op, ecs_bool_t, ecs_f64_t);\
    } else if (left->type == ecs_id(ecs_u8_t)) { \
        ECS_BINARY_OP_T(left, right, result, op, ecs_bool_t, ecs_u8_t);\
    } else if (left->type == ecs_id(ecs_char_t)) { \
        ECS_BINARY_OP_T(left, right, result, op, ecs_bool_t, ecs_char_t);\
    } else if (left->type == ecs_id(ecs_bool_t)) { \
        ECS_BINARY_OP_T(left, right, result, op, ecs_bool_t, ecs_bool_t);\
    } else {\
        ecs_abort(ECS_INTERNAL_ERROR, "unexpected type in binary expression");\
    }

#define ECS_BINARY_BOOL_OP(left, right, result, op)\
    if (left->type == ecs_id(ecs_bool_t)) { \
        ECS_BINARY_OP_T(left, right, result, op, ecs_bool_t, ecs_bool_t);\
    } else {\
        ecs_abort(ECS_INTERNAL_ERROR, "unexpected type in binary expression");\
    }

#define ECS_BINARY_UINT_OP(left, right, result, op)\
    if (left->type == ecs_id(ecs_u64_t)) { \
        ECS_BINARY_OP_T(left, right, result, op, ecs_u64_t, ecs_u64_t);\
    } else {\
        ecs_abort(ECS_INTERNAL_ERROR, "unexpected type in binary expression");\
    }

static
int flecs_binary_expr_do(
    ecs_world_t *world,
    ecs_value_stack_t *stack,
    const char *name,
    const char *expr,
    const char *ptr,
    ecs_value_t *lvalue,
    ecs_value_t *rvalue,
    ecs_value_t *result,
    ecs_expr_oper_t op)
{
    /* Find expression type */
    ecs_entity_t operand_type, type = flecs_binary_expr_type(
        world, name, expr, ptr, lvalue, rvalue, op, &operand_type);
    if (!type) {
        return -1;
    }

    if (!flecs_oper_valid_for_type(type, op)) {
        ecs_parser_error(name, expr, ptr - expr, "invalid operator for type");
        return -1;
    }

    flecs_value_cast(world, stack, lvalue, operand_type);
    flecs_value_cast(world, stack, rvalue, operand_type);

    ecs_value_t *storage = result;
    ecs_value_t tmp_storage = {0};
    if (result->type != type) {
        storage = &tmp_storage;
        storage->type = type;
        storage->ptr = flecs_expr_value_new(stack, type);
    }

    switch(op) {
    case EcsAdd:
        ECS_BINARY_OP(lvalue, rvalue, storage, +);
        break;
    case EcsSub:
        ECS_BINARY_OP(lvalue, rvalue, storage, -);
        break;
    case EcsMul:
        ECS_BINARY_OP(lvalue, rvalue, storage, *);
        break;
    case EcsDiv:
        ECS_BINARY_OP(lvalue, rvalue, storage, /);
        break;
    case EcsCondEq:
        ECS_BINARY_COND_OP(lvalue, rvalue, storage, ==);
        break;
    case EcsCondNeq:
        ECS_BINARY_COND_OP(lvalue, rvalue, storage, !=);
        break;
    case EcsCondGt:
        ECS_BINARY_COND_OP(lvalue, rvalue, storage, >);
        break;
    case EcsCondGtEq:
        ECS_BINARY_COND_OP(lvalue, rvalue, storage, >=);
        break;
    case EcsCondLt:
        ECS_BINARY_COND_OP(lvalue, rvalue, storage, <);
        break;
    case EcsCondLtEq:
        ECS_BINARY_COND_OP(lvalue, rvalue, storage, <=);
        break;
    case EcsCondAnd:
        ECS_BINARY_BOOL_OP(lvalue, rvalue, storage, &&);
        break;
    case EcsCondOr:
        ECS_BINARY_BOOL_OP(lvalue, rvalue, storage, ||);
        break;
    case EcsShiftLeft:
        ECS_BINARY_UINT_OP(lvalue, rvalue, storage, <<);
        break;
    case EcsShiftRight:
        ECS_BINARY_UINT_OP(lvalue, rvalue, storage, >>);
        break;
    default:
        ecs_parser_error(name, expr, ptr - expr, "unsupported operator");
        return -1;
    }

    if (storage->ptr != result->ptr) {
        if (!result->ptr) {
            *result = *storage;
        } else {
            ecs_meta_cursor_t cur = ecs_meta_cursor(world, 
                result->type, result->ptr);
            ecs_meta_set_value(&cur, storage);
        }
    }

    return 0;
}

static
const char* flecs_binary_expr_parse(
    ecs_world_t *world,
    ecs_value_stack_t *stack,
    const char *name,
    const char *expr,
    const char *ptr,
    ecs_value_t *lvalue,
    ecs_value_t *result,
    ecs_expr_oper_t left_op,
    const ecs_parse_expr_desc_t *desc)
{
    ecs_entity_t result_type = result->type;
    do {
        ecs_expr_oper_t op;
        ptr = flecs_str_to_expr_oper(ptr, &op);
        if (!ptr) {
            ecs_parser_error(name, expr, ptr - expr, "invalid operator");
            return NULL;
        }

        ptr = ecs_parse_ws_eol(ptr);

        ecs_value_t rvalue = {0};
        const char *rptr = flecs_parse_expr(world, stack, ptr, &rvalue, op, desc);
        if (!rptr) {
            return NULL;
        }

        if (flecs_binary_expr_do(world, stack, name, expr, ptr, 
            lvalue, &rvalue, result, op)) 
        {
            return NULL;
        }

        ptr = rptr;

        ecs_expr_oper_t right_op;
        flecs_str_to_expr_oper(rptr, &right_op);
        if (right_op > left_op) {
            if (result_type) {
                /* If result was initialized, preserve its value */
                lvalue->type = result->type;
                lvalue->ptr = flecs_expr_value_new(stack, lvalue->type);
                ecs_value_copy(world, lvalue->type, lvalue->ptr, result->ptr);
                continue;
            } else {
                /* Otherwise move result to lvalue */
                *lvalue = *result;
                ecs_os_zeromem(result);
                continue;
            }
        }

        break;
    } while (true);

    return ptr;
}

static
const char* flecs_parse_expr(
    ecs_world_t *world,
    ecs_value_stack_t *stack,
    const char *ptr,
    ecs_value_t *value,
    ecs_expr_oper_t left_op,
    const ecs_parse_expr_desc_t *desc)
{
    ecs_assert(value != NULL, ECS_INTERNAL_ERROR, NULL);
    char token[ECS_MAX_TOKEN_SIZE];
    int depth = 0;
    ecs_value_t result = {0};
    ecs_meta_cursor_t cur = {0};
    const char *name = desc ? desc->name : NULL;
    const char *expr = desc ? desc->expr : NULL;
    token[0] = '\0';
    expr = expr ? expr : ptr;

    ptr = ecs_parse_ws_eol(ptr);

    /* Check for postfix operators */
    ecs_expr_oper_t unary_op = EcsExprOperUnknown;
    if (ptr[0] == '-' && !isdigit(ptr[1])) {
        unary_op = EcsMin;
        ptr = ecs_parse_ws_eol(ptr + 1);
    }

    /* Initialize storage and cursor. If expression starts with a '(' storage
     * will be initialized by a nested expression */
    if (ptr[0] != '(') {
        ecs_entity_t type = flecs_parse_discover_type(
            world, name, expr, ptr, value->type, desc);
        if (!type) {
            return NULL;
        }

        result.type = type;
        if (type != value->type) {
            result.ptr = flecs_expr_value_new(stack, type);
        } else {
            result.ptr = value->ptr;
        }

        cur = ecs_meta_cursor(world, result.type, result.ptr);
        if (!cur.valid) {
            return NULL;
        }

        cur.lookup_action = desc ? desc->lookup_action : NULL;
        cur.lookup_ctx = desc ? desc->lookup_ctx : NULL;
    }

    /* Loop that parses all values in a value scope */
    while ((ptr = ecs_parse_expr_token(name, expr, ptr, token))) {
        /* Used to track of the result of the parsed token can be used as the
         * lvalue for a binary expression */
        bool is_lvalue = false;
        bool newline = false;

        if (!ecs_os_strcmp(token, "(")) {
            ecs_value_t temp_result, *out;
            if (!depth) {
                out = &result;
            } else {
                temp_result.type = ecs_meta_get_type(&cur);
                temp_result.ptr = ecs_meta_get_ptr(&cur);
                out = &temp_result;
            }

            /* Parenthesis, parse nested expression */
            ptr = flecs_parse_expr(world, stack, ptr, out, EcsLeftParen, desc);
            if (ptr[0] != ')') {
                ecs_parser_error(name, expr, ptr - expr, 
                    "missing closing parenthesis");
                return NULL;
            }
            ptr = ecs_parse_ws(ptr + 1);
            is_lvalue = true;

        } else if (!ecs_os_strcmp(token, "{")) {
            /* Parse nested value scope */
            ecs_entity_t scope_type = ecs_meta_get_type(&cur);

            depth ++; /* Keep track of depth so we know when parsing is done */
            if (ecs_meta_push(&cur) != 0) {
                goto error;
            }

            if (ecs_meta_is_collection(&cur)) {
                char *path = ecs_get_fullpath(world, scope_type);
                ecs_parser_error(name, expr, ptr - expr, 
                    "expected '[' for collection type '%s'", path);
                ecs_os_free(path);
                return NULL;
            }
        }

        else if (!ecs_os_strcmp(token, "}")) {
            depth --;

            if (ecs_meta_is_collection(&cur)) {
                ecs_parser_error(name, expr, ptr - expr, "expected ']'");
                return NULL;
            }

            if (ecs_meta_pop(&cur) != 0) {
                goto error;
            }
        }

        else if (!ecs_os_strcmp(token, "[")) {
            /* Open collection value scope */
            depth ++;
            if (ecs_meta_push(&cur) != 0) {
                goto error;
            }

            if (!ecs_meta_is_collection(&cur)) {
                ecs_parser_error(name, expr, ptr - expr, "expected '{'");
                return NULL;
            }
        }

        else if (!ecs_os_strcmp(token, "]")) {
            depth --;

            if (!ecs_meta_is_collection(&cur)) {
                ecs_parser_error(name, expr, ptr - expr, "expected '}'");
                return NULL;
            }

            if (ecs_meta_pop(&cur) != 0) {
                goto error;
            }
        }

        else if (!ecs_os_strcmp(token, "-")) {
            if (unary_op != EcsExprOperUnknown) {
                ecs_parser_error(name, expr, ptr - expr, 
                    "unexpected unary operator");
                return NULL;
            }
            unary_op = EcsMin;
        }

        else if (!ecs_os_strcmp(token, ",")) {
            /* Move to next field */
            if (ecs_meta_next(&cur) != 0) {
                goto error;
            }
        }

        else if (!ecs_os_strcmp(token, "null")) {
            if (ecs_meta_set_null(&cur) != 0) {
                goto error;
            }

            is_lvalue = true;
        }

        else if (token[0] == '\"') {
            /* Regular string */
            if (ecs_meta_set_string_literal(&cur, token) != 0) {
                goto error;
            }

            is_lvalue = true;
        }

        else if (!ecs_os_strcmp(token, "`")) {
            /* Multiline string */
            if (!(ptr = flecs_parse_multiline_string(&cur, name, expr, ptr))) {
                goto error;
            }

            is_lvalue = true;

        } else if (token[0] == '$') {
            /* Variable */
            if (!desc || !desc->vars) {
                ecs_parser_error(name, expr, ptr - expr, 
                    "unresolved variable '%s' (no variable scope)", token);
                return NULL;
            }

            if (!token[1]) {
                /* Empty name means default to assigned member */
                const char *member = ecs_meta_get_member(&cur);
                if (!member) {
                    ecs_parser_error(name, expr, ptr - expr, 
                        "invalid default variable outside member assignment");
                    return NULL;
                }
                ecs_os_strcpy(&token[1], member);
            }

            const ecs_expr_var_t *var = ecs_vars_lookup(desc->vars, &token[1]);
            if (!var) {
                ecs_value_t v = flecs_dotresolve_var(world, desc->vars, &token[1]);
                if (!v.ptr) {
                    ecs_parser_error(name, expr, ptr - expr, 
                        "unresolved variable '%s'", token);
                    return NULL;
                } else {
                    ecs_meta_set_value(&cur, &v);
                }
            } else {
                ecs_meta_set_value(&cur, &var->value);
            }
            is_lvalue = true;

        } else {
            const char *tptr = ecs_parse_ws(ptr);
            for (; ptr != tptr; ptr ++) {
                if (ptr[0] == '\n') {
                    newline = true;
                }
            }

            if (ptr[0] == ':') {
                /* Member assignment */
                ptr ++;
                if (ecs_meta_dotmember(&cur, token) != 0) {
                    goto error;
                }
            } else {
                if (ecs_meta_set_string(&cur, token) != 0) {
                    goto error;
                }
            }

            is_lvalue = true;
        }

        /* If lvalue was parsed, apply operators. Expressions cannot start
         * directly after a newline character. */
        if (is_lvalue && !newline) {
            if (unary_op != EcsExprOperUnknown) {
                if (unary_op == EcsMin) {
                    int64_t v = -1;
                    ecs_value_t lvalue = {.type = ecs_id(ecs_i64_t), .ptr = &v};
                    ecs_value_t *out, rvalue, temp_out = {0};

                    if (!depth) {
                        rvalue = result;
                        ecs_os_zeromem(&result);
                        out = &result;
                    } else {
                        ecs_entity_t cur_type = ecs_meta_get_type(&cur);
                        void *cur_ptr = ecs_meta_get_ptr(&cur);
                        rvalue.type = cur_type;
                        rvalue.ptr = cur_ptr;
                        temp_out.type = cur_type;
                        temp_out.ptr = cur_ptr;
                        out = &temp_out;
                    }

                    flecs_binary_expr_do(world, stack, name, expr, ptr, &lvalue,
                        &rvalue, out, EcsMul);
                }
                unary_op = 0;
            }

            ecs_expr_oper_t right_op;
            flecs_str_to_expr_oper(ptr, &right_op);
            if (right_op) {
                /* This is a binary expression, test precedence to determine if
                 * it should be evaluated here */
                if (flecs_oper_precedence(left_op, right_op) < 0) {
                    ecs_value_t lvalue;
                    ecs_value_t *op_result = &result;
                    ecs_value_t temp_storage;
                    if (!depth) {
                        /* Root level value, move result to lvalue storage */
                        lvalue = result;
                        ecs_os_zeromem(&result);
                    } else {
                        /* Not a root level value. Move the parsed lvalue to a
                         * temporary storage, and initialize the result value
                         * for the binary operation with the current cursor */
                        ecs_entity_t cur_type = ecs_meta_get_type(&cur);
                        void *cur_ptr = ecs_meta_get_ptr(&cur);
                        lvalue.type = cur_type;
                        lvalue.ptr = flecs_expr_value_new(stack, cur_type);
                        ecs_value_copy(world, cur_type, lvalue.ptr, cur_ptr);
                        temp_storage.type = cur_type;
                        temp_storage.ptr = cur_ptr;
                        op_result = &temp_storage;
                    }

                    /* Do the binary expression */
                    ptr = flecs_binary_expr_parse(world, stack, name, expr, ptr, 
                        &lvalue, op_result, left_op, desc);
                    if (!ptr) {
                        return NULL;
                    }
                }
            }
        }

        if (!depth) {
            /* Reached the end of the root scope */
            break;
        }

        ptr = ecs_parse_ws_eol(ptr);
    }

    if (!value->ptr) {
        value->type = result.type;
        value->ptr = flecs_expr_value_new(stack, result.type);
    }

    if (value->ptr != result.ptr) {
        cur = ecs_meta_cursor(world, value->type, value->ptr);
        ecs_meta_set_value(&cur, &result);
    }

    ecs_assert(value->type != 0, ECS_INTERNAL_ERROR, NULL);
    ecs_assert(value->ptr != NULL, ECS_INTERNAL_ERROR, NULL);

    return ptr;
error:
    return NULL;
}

const char* ecs_parse_expr(
    ecs_world_t *world,
    const char *ptr,
    ecs_value_t *value,
    const ecs_parse_expr_desc_t *desc)
{
    /* Prepare storage for temporary values */
    ecs_stage_t *stage = flecs_stage_from_world(&world);
    ecs_value_stack_t stack;
    stack.count = 0;
    stack.stage = stage;
    stack.stack = &stage->allocators.deser_stack;
    stack.cursor = flecs_stack_get_cursor(stack.stack);

    /* Parse expression */
    bool storage_provided = value->ptr != NULL;
    ptr = flecs_parse_expr(world, &stack, ptr, value, EcsExprOperUnknown, desc);

    /* If no result value was provided, allocate one as we can't return a 
     * pointer to a temporary storage */
    if (!storage_provided && value->ptr) {
        ecs_assert(value->type != 0, ECS_INTERNAL_ERROR, NULL);
        void *temp_storage = value->ptr;
        value->ptr = ecs_value_new(world, value->type);
        ecs_value_move_ctor(world, value->type, value->ptr, temp_storage);
    }
    
    /* Cleanup temporary values */
    int i;
    for (i = 0; i < stack.count; i ++) {
        const ecs_type_info_t *ti = stack.values[i].ti;
        ecs_assert(ti->hooks.dtor != NULL, ECS_INTERNAL_ERROR, NULL);
        ti->hooks.dtor(stack.values[i].ptr, 1, ti);
    }
    flecs_stack_restore_cursor(stack.stack, &stack.cursor);

    return ptr;
}

#endif

/**
 * @file addons/stats.c
 * @brief Stats addon.
 */


#ifdef FLECS_SYSTEM
#endif

#ifdef FLECS_PIPELINE
#endif

#ifdef FLECS_STATS

#define ECS_GAUGE_RECORD(m, t, value)\
    flecs_gauge_record(m, t, (ecs_float_t)(value))

#define ECS_COUNTER_RECORD(m, t, value)\
    flecs_counter_record(m, t, (double)(value))

#define ECS_METRIC_FIRST(stats)\
    ECS_CAST(ecs_metric_t*, ECS_OFFSET(&stats->first_, ECS_SIZEOF(int64_t)))

#define ECS_METRIC_LAST(stats)\
    ECS_CAST(ecs_metric_t*, ECS_OFFSET(&stats->last_, -ECS_SIZEOF(ecs_metric_t)))

static
int32_t t_next(
    int32_t t)
{
    return (t + 1) % ECS_STAT_WINDOW;
}

static
int32_t t_prev(
    int32_t t)
{
    return (t - 1 + ECS_STAT_WINDOW) % ECS_STAT_WINDOW;
}

static
void flecs_gauge_record(
    ecs_metric_t *m,
    int32_t t,
    ecs_float_t value)
{
    m->gauge.avg[t] = value;
    m->gauge.min[t] = value;
    m->gauge.max[t] = value;
}

static
double flecs_counter_record(
    ecs_metric_t *m,
    int32_t t,
    double value)
{
    int32_t tp = t_prev(t);
    double prev = m->counter.value[tp];
    m->counter.value[t] = value;
    double gauge_value = value - prev;
    if (gauge_value < 0) {
        gauge_value = 0; /* Counters are monotonically increasing */
    }
    flecs_gauge_record(m, t, (ecs_float_t)gauge_value);
    return gauge_value;
}

static
void flecs_metric_print(
    const char *name,
    ecs_float_t value)
{
    ecs_size_t len = ecs_os_strlen(name);
    ecs_trace("%s: %*s %.2f", name, 32 - len, "", (double)value);
}

static
void flecs_gauge_print(
    const char *name,
    int32_t t,
    const ecs_metric_t *m)
{
    flecs_metric_print(name, m->gauge.avg[t]);
}

static
void flecs_counter_print(
    const char *name,
    int32_t t,
    const ecs_metric_t *m)
{
    flecs_metric_print(name, m->counter.rate.avg[t]);
}

void ecs_metric_reduce(
    ecs_metric_t *dst,
    const ecs_metric_t *src,
    int32_t t_dst,
    int32_t t_src)
{
    ecs_check(dst != NULL, ECS_INVALID_PARAMETER, NULL);
    ecs_check(src != NULL, ECS_INVALID_PARAMETER, NULL);

    bool min_set = false;
    dst->gauge.avg[t_dst] = 0;
    dst->gauge.min[t_dst] = 0;
    dst->gauge.max[t_dst] = 0;

    ecs_float_t fwindow = (ecs_float_t)ECS_STAT_WINDOW;

    int32_t i;
    for (i = 0; i < ECS_STAT_WINDOW; i ++) {
        int32_t t = (t_src + i) % ECS_STAT_WINDOW;
        dst->gauge.avg[t_dst] += src->gauge.avg[t] / fwindow;

        if (!min_set || (src->gauge.min[t] < dst->gauge.min[t_dst])) {
            dst->gauge.min[t_dst] = src->gauge.min[t];
            min_set = true;
        }
        if ((src->gauge.max[t] > dst->gauge.max[t_dst])) {
            dst->gauge.max[t_dst] = src->gauge.max[t];
        }
    }

    dst->counter.value[t_dst] = src->counter.value[t_src];

error:
    return;
}

void ecs_metric_reduce_last(
    ecs_metric_t *m,
    int32_t prev,
    int32_t count)
{
    ecs_check(m != NULL, ECS_INVALID_PARAMETER, NULL);
    int32_t t = t_next(prev);

    if (m->gauge.min[t] < m->gauge.min[prev]) {
        m->gauge.min[prev] = m->gauge.min[t];
    }

    if (m->gauge.max[t] > m->gauge.max[prev]) {
        m->gauge.max[prev] = m->gauge.max[t];
    }

    ecs_float_t fcount = (ecs_float_t)(count + 1);
    ecs_float_t cur = m->gauge.avg[prev];
    ecs_float_t next = m->gauge.avg[t];

    cur *= ((fcount - 1) / fcount);
    next *= 1 / fcount;

    m->gauge.avg[prev] = cur + next;
    m->counter.value[prev] = m->counter.value[t];

error:
    return;
}

void ecs_metric_copy(
    ecs_metric_t *m,
    int32_t dst,
    int32_t src)
{
    ecs_check(m != NULL, ECS_INVALID_PARAMETER, NULL);
    ecs_check(dst != src, ECS_INVALID_PARAMETER, NULL);

    m->gauge.avg[dst] = m->gauge.avg[src];
    m->gauge.min[dst] = m->gauge.min[src];
    m->gauge.max[dst] = m->gauge.max[src];
    m->counter.value[dst] = m->counter.value[src];

error:
    return;
}

static
void flecs_stats_reduce(
    ecs_metric_t *dst_cur,
    ecs_metric_t *dst_last,
    ecs_metric_t *src_cur,
    int32_t t_dst,
    int32_t t_src)
{
    for (; dst_cur <= dst_last; dst_cur ++, src_cur ++) {
        ecs_metric_reduce(dst_cur, src_cur, t_dst, t_src);
    }
}

static
void flecs_stats_reduce_last(
    ecs_metric_t *dst_cur,
    ecs_metric_t *dst_last,
    ecs_metric_t *src_cur,
    int32_t t_dst,
    int32_t t_src,
    int32_t count)
{
    int32_t t_dst_next = t_next(t_dst);
    for (; dst_cur <= dst_last; dst_cur ++, src_cur ++) {
        /* Reduce into previous value */
        ecs_metric_reduce_last(dst_cur, t_dst, count);

        /* Restore old value */
        dst_cur->gauge.avg[t_dst_next] = src_cur->gauge.avg[t_src];
        dst_cur->gauge.min[t_dst_next] = src_cur->gauge.min[t_src];
        dst_cur->gauge.max[t_dst_next] = src_cur->gauge.max[t_src];
        dst_cur->counter.value[t_dst_next] = src_cur->counter.value[t_src];
    }
}

static
void flecs_stats_repeat_last(
    ecs_metric_t *cur,
    ecs_metric_t *last,
    int32_t t)
{
    int32_t prev = t_prev(t);
    for (; cur <= last; cur ++) {
        ecs_metric_copy(cur, t, prev);
    }
}

static
void flecs_stats_copy_last(
    ecs_metric_t *dst_cur,
    ecs_metric_t *dst_last,
    ecs_metric_t *src_cur,
    int32_t t_dst,
    int32_t t_src)
{
    for (; dst_cur <= dst_last; dst_cur ++, src_cur ++) {
        dst_cur->gauge.avg[t_dst] = src_cur->gauge.avg[t_src];
        dst_cur->gauge.min[t_dst] = src_cur->gauge.min[t_src];
        dst_cur->gauge.max[t_dst] = src_cur->gauge.max[t_src];
        dst_cur->counter.value[t_dst] = src_cur->counter.value[t_src];
    }
}

void ecs_world_stats_get(
    const ecs_world_t *world,
    ecs_world_stats_t *s)
{
    ecs_check(world != NULL, ECS_INVALID_PARAMETER, NULL);
    ecs_check(s != NULL, ECS_INVALID_PARAMETER, NULL);

    world = ecs_get_world(world);

    int32_t t = s->t = t_next(s->t);

    double delta_frame_count = 
    ECS_COUNTER_RECORD(&s->frame.frame_count, t, world->info.frame_count_total);
    ECS_COUNTER_RECORD(&s->frame.merge_count, t, world->info.merge_count_total);
    ECS_COUNTER_RECORD(&s->frame.rematch_count, t, world->info.rematch_count_total);
    ECS_COUNTER_RECORD(&s->frame.pipeline_build_count, t, world->info.pipeline_build_count_total);
    ECS_COUNTER_RECORD(&s->frame.systems_ran, t, world->info.systems_ran_frame);
    ECS_COUNTER_RECORD(&s->frame.observers_ran, t, world->info.observers_ran_frame);
    ECS_COUNTER_RECORD(&s->frame.event_emit_count, t, world->event_id);

    double delta_world_time = 
    ECS_COUNTER_RECORD(&s->performance.world_time_raw, t, world->info.world_time_total_raw);
    ECS_COUNTER_RECORD(&s->performance.world_time, t, world->info.world_time_total);
    ECS_COUNTER_RECORD(&s->performance.frame_time, t, world->info.frame_time_total);
    ECS_COUNTER_RECORD(&s->performance.system_time, t, world->info.system_time_total);
    ECS_COUNTER_RECORD(&s->performance.emit_time, t, world->info.emit_time_total);
    ECS_COUNTER_RECORD(&s->performance.merge_time, t, world->info.merge_time_total);
    ECS_COUNTER_RECORD(&s->performance.rematch_time, t, world->info.rematch_time_total);
    ECS_GAUGE_RECORD(&s->performance.delta_time, t, delta_world_time);
    if (delta_world_time != 0 && delta_frame_count != 0) {
        ECS_GAUGE_RECORD(&s->performance.fps, t, (double)1 / (delta_world_time / (double)delta_frame_count));
    } else {
        ECS_GAUGE_RECORD(&s->performance.fps, t, 0);
    }

    ECS_GAUGE_RECORD(&s->entities.count, t, flecs_entities_count(world));
    ECS_GAUGE_RECORD(&s->entities.not_alive_count, t, flecs_entities_not_alive_count(world));

    ECS_GAUGE_RECORD(&s->ids.count, t, world->info.id_count);
    ECS_GAUGE_RECORD(&s->ids.tag_count, t, world->info.tag_id_count);
    ECS_GAUGE_RECORD(&s->ids.component_count, t, world->info.component_id_count);
    ECS_GAUGE_RECORD(&s->ids.pair_count, t, world->info.pair_id_count);
    ECS_GAUGE_RECORD(&s->ids.wildcard_count, t, world->info.wildcard_id_count);
    ECS_GAUGE_RECORD(&s->ids.type_count, t, ecs_sparse_count(&world->type_info));
    ECS_COUNTER_RECORD(&s->ids.create_count, t, world->info.id_create_total);
    ECS_COUNTER_RECORD(&s->ids.delete_count, t, world->info.id_delete_total);

    ECS_GAUGE_RECORD(&s->queries.query_count, t, ecs_count_id(world, EcsQuery));
    ECS_GAUGE_RECORD(&s->queries.observer_count, t, ecs_count_id(world, EcsObserver));
    if (ecs_is_alive(world, EcsSystem)) {
        ECS_GAUGE_RECORD(&s->queries.system_count, t, ecs_count_id(world, EcsSystem));
    }
    ECS_COUNTER_RECORD(&s->tables.create_count, t, world->info.table_create_total);
    ECS_COUNTER_RECORD(&s->tables.delete_count, t, world->info.table_delete_total);
    ECS_GAUGE_RECORD(&s->tables.count, t, world->info.table_count);
    ECS_GAUGE_RECORD(&s->tables.empty_count, t, world->info.empty_table_count);
    ECS_GAUGE_RECORD(&s->tables.tag_only_count, t, world->info.tag_table_count);
    ECS_GAUGE_RECORD(&s->tables.trivial_only_count, t, world->info.trivial_table_count);
    ECS_GAUGE_RECORD(&s->tables.storage_count, t, world->info.table_storage_count);
    ECS_GAUGE_RECORD(&s->tables.record_count, t, world->info.table_record_count);

    ECS_COUNTER_RECORD(&s->commands.add_count, t, world->info.cmd.add_count);
    ECS_COUNTER_RECORD(&s->commands.remove_count, t, world->info.cmd.remove_count);
    ECS_COUNTER_RECORD(&s->commands.delete_count, t, world->info.cmd.delete_count);
    ECS_COUNTER_RECORD(&s->commands.clear_count, t, world->info.cmd.clear_count);
    ECS_COUNTER_RECORD(&s->commands.set_count, t, world->info.cmd.set_count);
    ECS_COUNTER_RECORD(&s->commands.get_mut_count, t, world->info.cmd.get_mut_count);
    ECS_COUNTER_RECORD(&s->commands.modified_count, t, world->info.cmd.modified_count);
    ECS_COUNTER_RECORD(&s->commands.other_count, t, world->info.cmd.other_count);
    ECS_COUNTER_RECORD(&s->commands.discard_count, t, world->info.cmd.discard_count);
    ECS_COUNTER_RECORD(&s->commands.batched_entity_count, t, world->info.cmd.batched_entity_count);
    ECS_COUNTER_RECORD(&s->commands.batched_count, t, world->info.cmd.batched_command_count);

    int64_t outstanding_allocs = ecs_os_api_malloc_count + 
        ecs_os_api_calloc_count - ecs_os_api_free_count;
    ECS_COUNTER_RECORD(&s->memory.alloc_count, t, ecs_os_api_malloc_count + ecs_os_api_calloc_count);
    ECS_COUNTER_RECORD(&s->memory.realloc_count, t, ecs_os_api_realloc_count);
    ECS_COUNTER_RECORD(&s->memory.free_count, t, ecs_os_api_free_count);
    ECS_GAUGE_RECORD(&s->memory.outstanding_alloc_count, t, outstanding_allocs);

    outstanding_allocs = ecs_block_allocator_alloc_count - ecs_block_allocator_free_count;
    ECS_COUNTER_RECORD(&s->memory.block_alloc_count, t, ecs_block_allocator_alloc_count);
    ECS_COUNTER_RECORD(&s->memory.block_free_count, t, ecs_block_allocator_free_count);
    ECS_GAUGE_RECORD(&s->memory.block_outstanding_alloc_count, t, outstanding_allocs);

    outstanding_allocs = ecs_stack_allocator_alloc_count - ecs_stack_allocator_free_count;
    ECS_COUNTER_RECORD(&s->memory.stack_alloc_count, t, ecs_stack_allocator_alloc_count);
    ECS_COUNTER_RECORD(&s->memory.stack_free_count, t, ecs_stack_allocator_free_count);
    ECS_GAUGE_RECORD(&s->memory.stack_outstanding_alloc_count, t, outstanding_allocs);

#ifdef FLECS_REST
    ECS_COUNTER_RECORD(&s->rest.request_count, t, ecs_rest_request_count);
    ECS_COUNTER_RECORD(&s->rest.entity_count, t, ecs_rest_entity_count);
    ECS_COUNTER_RECORD(&s->rest.entity_error_count, t, ecs_rest_entity_error_count);
    ECS_COUNTER_RECORD(&s->rest.query_count, t, ecs_rest_query_count);
    ECS_COUNTER_RECORD(&s->rest.query_error_count, t, ecs_rest_query_error_count);
    ECS_COUNTER_RECORD(&s->rest.query_name_count, t, ecs_rest_query_name_count);
    ECS_COUNTER_RECORD(&s->rest.query_name_error_count, t, ecs_rest_query_name_error_count);
    ECS_COUNTER_RECORD(&s->rest.query_name_from_cache_count, t, ecs_rest_query_name_from_cache_count);
    ECS_COUNTER_RECORD(&s->rest.enable_count, t, ecs_rest_enable_count);
    ECS_COUNTER_RECORD(&s->rest.enable_error_count, t, ecs_rest_enable_error_count);
    ECS_COUNTER_RECORD(&s->rest.world_stats_count, t, ecs_rest_world_stats_count);
    ECS_COUNTER_RECORD(&s->rest.pipeline_stats_count, t, ecs_rest_pipeline_stats_count);
    ECS_COUNTER_RECORD(&s->rest.stats_error_count, t, ecs_rest_stats_error_count);
#endif

#ifdef FLECS_HTTP
    ECS_COUNTER_RECORD(&s->http.request_received_count, t, ecs_http_request_received_count);
    ECS_COUNTER_RECORD(&s->http.request_invalid_count, t, ecs_http_request_invalid_count);
    ECS_COUNTER_RECORD(&s->http.request_handled_ok_count, t, ecs_http_request_handled_ok_count);
    ECS_COUNTER_RECORD(&s->http.request_handled_error_count, t, ecs_http_request_handled_error_count);
    ECS_COUNTER_RECORD(&s->http.request_not_handled_count, t, ecs_http_request_not_handled_count);
    ECS_COUNTER_RECORD(&s->http.request_preflight_count, t, ecs_http_request_preflight_count);
    ECS_COUNTER_RECORD(&s->http.send_ok_count, t, ecs_http_send_ok_count);
    ECS_COUNTER_RECORD(&s->http.send_error_count, t, ecs_http_send_error_count);
    ECS_COUNTER_RECORD(&s->http.busy_count, t, ecs_http_busy_count);
#endif

error:
    return;
}

void ecs_world_stats_reduce(
    ecs_world_stats_t *dst,
    const ecs_world_stats_t *src)
{
    flecs_stats_reduce(ECS_METRIC_FIRST(dst), ECS_METRIC_LAST(dst), 
        ECS_METRIC_FIRST(src), (dst->t = t_next(dst->t)), src->t);
}

void ecs_world_stats_reduce_last(
    ecs_world_stats_t *dst,
    const ecs_world_stats_t *src,
    int32_t count)
{
    flecs_stats_reduce_last(ECS_METRIC_FIRST(dst), ECS_METRIC_LAST(dst), 
        ECS_METRIC_FIRST(src), (dst->t = t_prev(dst->t)), src->t, count);
}

void ecs_world_stats_repeat_last(
    ecs_world_stats_t *stats)
{
    flecs_stats_repeat_last(ECS_METRIC_FIRST(stats), ECS_METRIC_LAST(stats),
        (stats->t = t_next(stats->t)));
}

void ecs_world_stats_copy_last(
    ecs_world_stats_t *dst,
    const ecs_world_stats_t *src)
{
    flecs_stats_copy_last(ECS_METRIC_FIRST(dst), ECS_METRIC_LAST(dst),
        ECS_METRIC_FIRST(src), dst->t, t_next(src->t));
}

void ecs_query_stats_get(
    const ecs_world_t *world,
    const ecs_query_t *query,
    ecs_query_stats_t *s)
{
    ecs_check(world != NULL, ECS_INVALID_PARAMETER, NULL);
    ecs_check(query != NULL, ECS_INVALID_PARAMETER, NULL);
    ecs_check(s != NULL, ECS_INVALID_PARAMETER, NULL);
    (void)world;

    int32_t t = s->t = t_next(s->t);

    if (query->filter.flags & EcsFilterMatchThis) {
        ECS_GAUGE_RECORD(&s->matched_entity_count, t, 
            ecs_query_entity_count(query));
        ECS_GAUGE_RECORD(&s->matched_table_count, t, 
            ecs_query_table_count(query));
        ECS_GAUGE_RECORD(&s->matched_empty_table_count, t, 
            ecs_query_empty_table_count(query));
    } else {
        ECS_GAUGE_RECORD(&s->matched_entity_count, t, 0);
        ECS_GAUGE_RECORD(&s->matched_table_count, t, 0);
        ECS_GAUGE_RECORD(&s->matched_empty_table_count, t, 0);
    }

error:
    return;
}

void ecs_query_stats_reduce(
    ecs_query_stats_t *dst,
    const ecs_query_stats_t *src)
{
    flecs_stats_reduce(ECS_METRIC_FIRST(dst), ECS_METRIC_LAST(dst), 
        ECS_METRIC_FIRST(src), (dst->t = t_next(dst->t)), src->t);
}

void ecs_query_stats_reduce_last(
    ecs_query_stats_t *dst,
    const ecs_query_stats_t *src,
    int32_t count)
{
    flecs_stats_reduce_last(ECS_METRIC_FIRST(dst), ECS_METRIC_LAST(dst), 
        ECS_METRIC_FIRST(src), (dst->t = t_prev(dst->t)), src->t, count);
}

void ecs_query_stats_repeat_last(
    ecs_query_stats_t *stats)
{
    flecs_stats_repeat_last(ECS_METRIC_FIRST(stats), ECS_METRIC_LAST(stats),
        (stats->t = t_next(stats->t)));
}

void ecs_query_stats_copy_last(
    ecs_query_stats_t *dst,
    const ecs_query_stats_t *src)
{
    flecs_stats_copy_last(ECS_METRIC_FIRST(dst), ECS_METRIC_LAST(dst),
        ECS_METRIC_FIRST(src), dst->t, t_next(src->t));
}

#ifdef FLECS_SYSTEM

bool ecs_system_stats_get(
    const ecs_world_t *world,
    ecs_entity_t system,
    ecs_system_stats_t *s)
{
    ecs_check(world != NULL, ECS_INVALID_PARAMETER, NULL);
    ecs_check(s != NULL, ECS_INVALID_PARAMETER, NULL);
    ecs_check(system != 0, ECS_INVALID_PARAMETER, NULL);

    world = ecs_get_world(world);

    const ecs_system_t *ptr = ecs_poly_get(world, system, ecs_system_t);
    if (!ptr) {
        return false;
    }

    ecs_query_stats_get(world, ptr->query, &s->query);
    int32_t t = s->query.t;

    ECS_COUNTER_RECORD(&s->time_spent, t, ptr->time_spent);
    ECS_COUNTER_RECORD(&s->invoke_count, t, ptr->invoke_count);
    ECS_GAUGE_RECORD(&s->active, t, !ecs_has_id(world, system, EcsEmpty));
    ECS_GAUGE_RECORD(&s->enabled, t, !ecs_has_id(world, system, EcsDisabled));

    s->task = !(ptr->query->filter.flags & EcsFilterMatchThis);

    return true;
error:
    return false;
}

void ecs_system_stats_reduce(
    ecs_system_stats_t *dst,
    const ecs_system_stats_t *src)
{
    ecs_query_stats_reduce(&dst->query, &src->query);
    dst->task = src->task;
    flecs_stats_reduce(ECS_METRIC_FIRST(dst), ECS_METRIC_LAST(dst), 
        ECS_METRIC_FIRST(src), dst->query.t, src->query.t);
}

void ecs_system_stats_reduce_last(
    ecs_system_stats_t *dst,
    const ecs_system_stats_t *src,
    int32_t count)
{
    ecs_query_stats_reduce_last(&dst->query, &src->query, count);
    dst->task = src->task;
    flecs_stats_reduce_last(ECS_METRIC_FIRST(dst), ECS_METRIC_LAST(dst), 
        ECS_METRIC_FIRST(src), dst->query.t, src->query.t, count);
}

void ecs_system_stats_repeat_last(
    ecs_system_stats_t *stats)
{
    ecs_query_stats_repeat_last(&stats->query);
    flecs_stats_repeat_last(ECS_METRIC_FIRST(stats), ECS_METRIC_LAST(stats),
        (stats->query.t));
}

void ecs_system_stats_copy_last(
    ecs_system_stats_t *dst,
    const ecs_system_stats_t *src)
{
    ecs_query_stats_copy_last(&dst->query, &src->query);
    dst->task = src->task;
    flecs_stats_copy_last(ECS_METRIC_FIRST(dst), ECS_METRIC_LAST(dst),
        ECS_METRIC_FIRST(src), dst->query.t, t_next(src->query.t));
}

#endif

#ifdef FLECS_PIPELINE

bool ecs_pipeline_stats_get(
    ecs_world_t *stage,
    ecs_entity_t pipeline,
    ecs_pipeline_stats_t *s)
{
    ecs_check(stage != NULL, ECS_INVALID_PARAMETER, NULL);
    ecs_check(s != NULL, ECS_INVALID_PARAMETER, NULL);
    ecs_check(pipeline != 0, ECS_INVALID_PARAMETER, NULL);

    const ecs_world_t *world = ecs_get_world(stage);
    const EcsPipeline *pqc = ecs_get(world, pipeline, EcsPipeline);
    if (!pqc) {
        return false;
    }
    ecs_pipeline_state_t *pq = pqc->state;
    ecs_assert(pq != NULL, ECS_INTERNAL_ERROR, NULL);

    int32_t sys_count = 0, active_sys_count = 0;

    /* Count number of active systems */
    ecs_iter_t it = ecs_query_iter(stage, pq->query);
    while (ecs_query_next(&it)) {
        if (flecs_id_record_get_table(pq->idr_inactive, it.table) != NULL) {
            continue;
        }
        active_sys_count += it.count;
    }

    /* Count total number of systems in pipeline */
    it = ecs_query_iter(stage, pq->query);
    while (ecs_query_next(&it)) {
        sys_count += it.count;
    }   

    /* Also count synchronization points */
    ecs_vec_t *ops = &pq->ops;
    ecs_pipeline_op_t *op = ecs_vec_first_t(ops, ecs_pipeline_op_t);
    ecs_pipeline_op_t *op_last = ecs_vec_last_t(ops, ecs_pipeline_op_t);
    int32_t pip_count = active_sys_count + ecs_vec_count(ops);

    if (!sys_count) {
        return false;
    }

    if (ecs_map_is_init(&s->system_stats) && !sys_count) {
        ecs_map_fini(&s->system_stats);
    }
    ecs_map_init_if(&s->system_stats, NULL);

    /* Make sure vector is large enough to store all systems & sync points */
    if (op) {
        ecs_entity_t *systems = NULL;
        if (pip_count) {
            ecs_vec_init_if_t(&s->systems, ecs_entity_t);
            ecs_vec_set_count_t(NULL, &s->systems, ecs_entity_t, pip_count);
            systems = ecs_vec_first_t(&s->systems, ecs_entity_t);

            /* Populate systems vector, keep track of sync points */
            it = ecs_query_iter(stage, pq->query);
            
            int32_t i, i_system = 0, ran_since_merge = 0;
            while (ecs_query_next(&it)) {
                if (flecs_id_record_get_table(pq->idr_inactive, it.table) != NULL) {
                    continue;
                }

                for (i = 0; i < it.count; i ++) {
                    systems[i_system ++] = it.entities[i];
                    ran_since_merge ++;
                    if (op != op_last && ran_since_merge == op->count) {
                        ran_since_merge = 0;
                        op++;
                        systems[i_system ++] = 0; /* 0 indicates a merge point */
                    }
                }
            }

            systems[i_system ++] = 0; /* Last merge */
            ecs_assert(pip_count == i_system, ECS_INTERNAL_ERROR, NULL);
        } else {
            ecs_vec_fini_t(NULL, &s->systems, ecs_entity_t);
        }
    }

    /* Separately populate system stats map from build query, which includes
     * systems that aren't currently active */
    it = ecs_query_iter(stage, pq->query);
    while (ecs_query_next(&it)) {
        int i;
        for (i = 0; i < it.count; i ++) {
            ecs_system_stats_t *stats = ecs_map_ensure_alloc_t(&s->system_stats, 
                ecs_system_stats_t, it.entities[i]);
            stats->query.t = s->t;
            ecs_system_stats_get(world, it.entities[i], stats);
        }
    }

    s->t = t_next(s->t);

    return true;
error:
    return false;
}

void ecs_pipeline_stats_fini(
    ecs_pipeline_stats_t *stats)
{
    ecs_map_iter_t it = ecs_map_iter(&stats->system_stats);
    while (ecs_map_next(&it)) {
        ecs_system_stats_t *elem = ecs_map_ptr(&it);
        ecs_os_free(elem);
    }
    ecs_map_fini(&stats->system_stats);
    ecs_vec_fini_t(NULL, &stats->systems, ecs_entity_t);
}

void ecs_pipeline_stats_reduce(
    ecs_pipeline_stats_t *dst,
    const ecs_pipeline_stats_t *src)
{
    int32_t system_count = ecs_vec_count(&src->systems);
    ecs_vec_init_if_t(&dst->systems, ecs_entity_t);
    ecs_vec_set_count_t(NULL, &dst->systems, ecs_entity_t, system_count);
    ecs_entity_t *dst_systems = ecs_vec_first_t(&dst->systems, ecs_entity_t);
    ecs_entity_t *src_systems = ecs_vec_first_t(&src->systems, ecs_entity_t);
    ecs_os_memcpy_n(dst_systems, src_systems, ecs_entity_t, system_count);

    ecs_map_init_if(&dst->system_stats, NULL);

    ecs_map_iter_t it = ecs_map_iter(&src->system_stats);
    
    while (ecs_map_next(&it)) {
        ecs_system_stats_t *sys_src = ecs_map_ptr(&it);
        ecs_system_stats_t *sys_dst = ecs_map_ensure_alloc_t(&dst->system_stats, 
            ecs_system_stats_t, ecs_map_key(&it));
        sys_dst->query.t = dst->t;
        ecs_system_stats_reduce(sys_dst, sys_src);
    }
    dst->t = t_next(dst->t);
}

void ecs_pipeline_stats_reduce_last(
    ecs_pipeline_stats_t *dst,
    const ecs_pipeline_stats_t *src,
    int32_t count)
{
    ecs_map_init_if(&dst->system_stats, NULL);
    ecs_map_iter_t it = ecs_map_iter(&src->system_stats);
    while (ecs_map_next(&it)) {
        ecs_system_stats_t *sys_src = ecs_map_ptr(&it);
        ecs_system_stats_t *sys_dst = ecs_map_ensure_alloc_t(&dst->system_stats, 
            ecs_system_stats_t, ecs_map_key(&it));
        sys_dst->query.t = dst->t;
        ecs_system_stats_reduce_last(sys_dst, sys_src, count);
    }
    dst->t = t_prev(dst->t);
}

void ecs_pipeline_stats_repeat_last(
    ecs_pipeline_stats_t *stats)
{
    ecs_map_iter_t it = ecs_map_iter(&stats->system_stats);
    while (ecs_map_next(&it)) {
        ecs_system_stats_t *sys = ecs_map_ptr(&it);
        sys->query.t = stats->t;
        ecs_system_stats_repeat_last(sys);
    }
    stats->t = t_next(stats->t);
}

void ecs_pipeline_stats_copy_last(
    ecs_pipeline_stats_t *dst,
    const ecs_pipeline_stats_t *src)
{
    ecs_map_init_if(&dst->system_stats, NULL);

    ecs_map_iter_t it = ecs_map_iter(&src->system_stats);
    while (ecs_map_next(&it)) {
        ecs_system_stats_t *sys_src = ecs_map_ptr(&it);
        ecs_system_stats_t *sys_dst = ecs_map_ensure_alloc_t(&dst->system_stats, 
            ecs_system_stats_t, ecs_map_key(&it));
        sys_dst->query.t = dst->t;
        ecs_system_stats_copy_last(sys_dst, sys_src);
    }
}

#endif

void ecs_world_stats_log(
    const ecs_world_t *world,
    const ecs_world_stats_t *s)
{
    int32_t t = s->t;

    ecs_check(world != NULL, ECS_INVALID_PARAMETER, NULL);
    ecs_check(s != NULL, ECS_INVALID_PARAMETER, NULL);

    world = ecs_get_world(world);    
    
    flecs_counter_print("Frame", t, &s->frame.frame_count);
    ecs_trace("-------------------------------------");
    flecs_counter_print("pipeline rebuilds", t, &s->frame.pipeline_build_count);
    flecs_counter_print("systems ran", t, &s->frame.systems_ran);
    ecs_trace("");
    flecs_metric_print("target FPS", world->info.target_fps);
    flecs_metric_print("time scale", world->info.time_scale);
    ecs_trace("");
    flecs_gauge_print("actual FPS", t, &s->performance.fps);
    flecs_counter_print("frame time", t, &s->performance.frame_time);
    flecs_counter_print("system time", t, &s->performance.system_time);
    flecs_counter_print("merge time", t, &s->performance.merge_time);
    flecs_counter_print("simulation time elapsed", t, &s->performance.world_time);
    ecs_trace("");
    flecs_gauge_print("id count", t, &s->ids.count);
    flecs_gauge_print("tag id count", t, &s->ids.tag_count);
    flecs_gauge_print("component id count", t, &s->ids.component_count);
    flecs_gauge_print("pair id count", t, &s->ids.pair_count);
    flecs_gauge_print("wildcard id count", t, &s->ids.wildcard_count);
    flecs_gauge_print("type count", t, &s->ids.type_count);
    flecs_counter_print("id create count", t, &s->ids.create_count);
    flecs_counter_print("id delete count", t, &s->ids.delete_count);
    ecs_trace("");
    flecs_gauge_print("alive entity count", t, &s->entities.count);
    flecs_gauge_print("not alive entity count", t, &s->entities.not_alive_count);
    ecs_trace("");
    flecs_gauge_print("query count", t, &s->queries.query_count);
    flecs_gauge_print("observer count", t, &s->queries.observer_count);
    flecs_gauge_print("system count", t, &s->queries.system_count);
    ecs_trace("");
    flecs_gauge_print("table count", t, &s->tables.count);
    flecs_gauge_print("empty table count", t, &s->tables.empty_count);
    flecs_gauge_print("tag table count", t, &s->tables.tag_only_count);
    flecs_gauge_print("trivial table count", t, &s->tables.trivial_only_count);
    flecs_gauge_print("table storage count", t, &s->tables.storage_count);
    flecs_gauge_print("table cache record count", t, &s->tables.record_count);
    flecs_counter_print("table create count", t, &s->tables.create_count);
    flecs_counter_print("table delete count", t, &s->tables.delete_count);
    ecs_trace("");
    flecs_counter_print("add commands", t, &s->commands.add_count);
    flecs_counter_print("remove commands", t, &s->commands.remove_count);
    flecs_counter_print("delete commands", t, &s->commands.delete_count);
    flecs_counter_print("clear commands", t, &s->commands.clear_count);
    flecs_counter_print("set commands", t, &s->commands.set_count);
    flecs_counter_print("get_mut commands", t, &s->commands.get_mut_count);
    flecs_counter_print("modified commands", t, &s->commands.modified_count);
    flecs_counter_print("other commands", t, &s->commands.other_count);
    flecs_counter_print("discarded commands", t, &s->commands.discard_count);
    flecs_counter_print("batched entities", t, &s->commands.batched_entity_count);
    flecs_counter_print("batched commands", t, &s->commands.batched_count);
    ecs_trace("");
    
error:
    return;
}

#endif

/**
 * @file addons/units.c
 * @brief Units addon.
 */


#ifdef FLECS_UNITS

ECS_DECLARE(EcsUnitPrefixes);

ECS_DECLARE(EcsYocto);
ECS_DECLARE(EcsZepto);
ECS_DECLARE(EcsAtto);
ECS_DECLARE(EcsFemto);
ECS_DECLARE(EcsPico);
ECS_DECLARE(EcsNano);
ECS_DECLARE(EcsMicro);
ECS_DECLARE(EcsMilli);
ECS_DECLARE(EcsCenti);
ECS_DECLARE(EcsDeci);
ECS_DECLARE(EcsDeca);
ECS_DECLARE(EcsHecto);
ECS_DECLARE(EcsKilo);
ECS_DECLARE(EcsMega);
ECS_DECLARE(EcsGiga);
ECS_DECLARE(EcsTera);
ECS_DECLARE(EcsPeta);
ECS_DECLARE(EcsExa);
ECS_DECLARE(EcsZetta);
ECS_DECLARE(EcsYotta);

ECS_DECLARE(EcsKibi);
ECS_DECLARE(EcsMebi);
ECS_DECLARE(EcsGibi);
ECS_DECLARE(EcsTebi);
ECS_DECLARE(EcsPebi);
ECS_DECLARE(EcsExbi);
ECS_DECLARE(EcsZebi);
ECS_DECLARE(EcsYobi);

ECS_DECLARE(EcsDuration);
    ECS_DECLARE(EcsPicoSeconds);
    ECS_DECLARE(EcsNanoSeconds);
    ECS_DECLARE(EcsMicroSeconds);
    ECS_DECLARE(EcsMilliSeconds);
    ECS_DECLARE(EcsSeconds);
    ECS_DECLARE(EcsMinutes);
    ECS_DECLARE(EcsHours);
    ECS_DECLARE(EcsDays);

ECS_DECLARE(EcsTime);
    ECS_DECLARE(EcsDate);

ECS_DECLARE(EcsMass);
    ECS_DECLARE(EcsGrams);
    ECS_DECLARE(EcsKiloGrams);

ECS_DECLARE(EcsElectricCurrent);
    ECS_DECLARE(EcsAmpere);

ECS_DECLARE(EcsAmount);
    ECS_DECLARE(EcsMole);

ECS_DECLARE(EcsLuminousIntensity);
    ECS_DECLARE(EcsCandela);

ECS_DECLARE(EcsForce);
    ECS_DECLARE(EcsNewton);

ECS_DECLARE(EcsLength);
    ECS_DECLARE(EcsMeters);
        ECS_DECLARE(EcsPicoMeters);
        ECS_DECLARE(EcsNanoMeters);
        ECS_DECLARE(EcsMicroMeters);
        ECS_DECLARE(EcsMilliMeters);
        ECS_DECLARE(EcsCentiMeters);
        ECS_DECLARE(EcsKiloMeters);
    ECS_DECLARE(EcsMiles);
    ECS_DECLARE(EcsPixels);

ECS_DECLARE(EcsPressure);
    ECS_DECLARE(EcsPascal);
    ECS_DECLARE(EcsBar);

ECS_DECLARE(EcsSpeed);
    ECS_DECLARE(EcsMetersPerSecond);
    ECS_DECLARE(EcsKiloMetersPerSecond);
    ECS_DECLARE(EcsKiloMetersPerHour);
    ECS_DECLARE(EcsMilesPerHour);

ECS_DECLARE(EcsAcceleration);

ECS_DECLARE(EcsTemperature);
    ECS_DECLARE(EcsKelvin);
    ECS_DECLARE(EcsCelsius);
    ECS_DECLARE(EcsFahrenheit);

ECS_DECLARE(EcsData);
    ECS_DECLARE(EcsBits);
        ECS_DECLARE(EcsKiloBits);
        ECS_DECLARE(EcsMegaBits);
        ECS_DECLARE(EcsGigaBits);
    ECS_DECLARE(EcsBytes);
        ECS_DECLARE(EcsKiloBytes);
        ECS_DECLARE(EcsMegaBytes);
        ECS_DECLARE(EcsGigaBytes);
        ECS_DECLARE(EcsKibiBytes);
        ECS_DECLARE(EcsGibiBytes);
        ECS_DECLARE(EcsMebiBytes);

ECS_DECLARE(EcsDataRate);
    ECS_DECLARE(EcsBitsPerSecond);
    ECS_DECLARE(EcsKiloBitsPerSecond);
    ECS_DECLARE(EcsMegaBitsPerSecond);
    ECS_DECLARE(EcsGigaBitsPerSecond);
    ECS_DECLARE(EcsBytesPerSecond);
    ECS_DECLARE(EcsKiloBytesPerSecond);
    ECS_DECLARE(EcsMegaBytesPerSecond);
    ECS_DECLARE(EcsGigaBytesPerSecond);

ECS_DECLARE(EcsPercentage);

ECS_DECLARE(EcsAngle);
    ECS_DECLARE(EcsRadians);
    ECS_DECLARE(EcsDegrees);

ECS_DECLARE(EcsBel);
ECS_DECLARE(EcsDeciBel);

ECS_DECLARE(EcsFrequency);
    ECS_DECLARE(EcsHertz);
    ECS_DECLARE(EcsKiloHertz);
    ECS_DECLARE(EcsMegaHertz);
    ECS_DECLARE(EcsGigaHertz);

ECS_DECLARE(EcsUri);
    ECS_DECLARE(EcsUriHyperlink);
    ECS_DECLARE(EcsUriImage);
    ECS_DECLARE(EcsUriFile);

void FlecsUnitsImport(
    ecs_world_t *world)
{
    ECS_MODULE(world, FlecsUnits);

    ecs_set_name_prefix(world, "Ecs");

    EcsUnitPrefixes = ecs_entity(world, {
        .name = "prefixes",
        .add = { EcsModule }
    });

    /* Initialize unit prefixes */

    ecs_entity_t prev_scope = ecs_set_scope(world, EcsUnitPrefixes);

    EcsYocto = ecs_unit_prefix_init(world, &(ecs_unit_prefix_desc_t){
        .entity = ecs_entity(world, { .name = "Yocto" }),
        .symbol = "y",
        .translation = { .factor = 10, .power = -24 }
    });
    EcsZepto = ecs_unit_prefix_init(world, &(ecs_unit_prefix_desc_t){
        .entity = ecs_entity(world, { .name = "Zepto" }),
        .symbol = "z",
        .translation = { .factor = 10, .power = -21 }
    });
    EcsAtto = ecs_unit_prefix_init(world, &(ecs_unit_prefix_desc_t){
        .entity = ecs_entity(world, { .name = "Atto" }),
        .symbol = "a",
        .translation = { .factor = 10, .power = -18 }
    });
    EcsFemto = ecs_unit_prefix_init(world, &(ecs_unit_prefix_desc_t){
        .entity = ecs_entity(world, { .name = "Femto" }),
        .symbol = "a",
        .translation = { .factor = 10, .power = -15 }
    });
    EcsPico = ecs_unit_prefix_init(world, &(ecs_unit_prefix_desc_t){
        .entity = ecs_entity(world, { .name = "Pico" }),
        .symbol = "p",
        .translation = { .factor = 10, .power = -12 }
    });
    EcsNano = ecs_unit_prefix_init(world, &(ecs_unit_prefix_desc_t){
        .entity = ecs_entity(world, { .name = "Nano" }),
        .symbol = "n",
        .translation = { .factor = 10, .power = -9 }
    });
    EcsMicro = ecs_unit_prefix_init(world, &(ecs_unit_prefix_desc_t){
        .entity = ecs_entity(world, { .name = "Micro" }),
        .symbol = "μ",
        .translation = { .factor = 10, .power = -6 }
    });
    EcsMilli = ecs_unit_prefix_init(world, &(ecs_unit_prefix_desc_t){
        .entity = ecs_entity(world, { .name = "Milli" }),
        .symbol = "m",
        .translation = { .factor = 10, .power = -3 }
    });
    EcsCenti = ecs_unit_prefix_init(world, &(ecs_unit_prefix_desc_t){
        .entity = ecs_entity(world, { .name = "Centi" }),
        .symbol = "c",
        .translation = { .factor = 10, .power = -2 }
    });
    EcsDeci = ecs_unit_prefix_init(world, &(ecs_unit_prefix_desc_t){
        .entity = ecs_entity(world, { .name = "Deci" }),
        .symbol = "d",
        .translation = { .factor = 10, .power = -1 }
    });
    EcsDeca = ecs_unit_prefix_init(world, &(ecs_unit_prefix_desc_t){
        .entity = ecs_entity(world, { .name = "Deca" }),
        .symbol = "da",
        .translation = { .factor = 10, .power = 1 }
    });
    EcsHecto = ecs_unit_prefix_init(world, &(ecs_unit_prefix_desc_t){
        .entity = ecs_entity(world, { .name = "Hecto" }),
        .symbol = "h",
        .translation = { .factor = 10, .power = 2 }
    });
    EcsKilo = ecs_unit_prefix_init(world, &(ecs_unit_prefix_desc_t){
        .entity = ecs_entity(world, { .name = "Kilo" }),
        .symbol = "k",
        .translation = { .factor = 10, .power = 3 }
    });
    EcsMega = ecs_unit_prefix_init(world, &(ecs_unit_prefix_desc_t){
        .entity = ecs_entity(world, { .name = "Mega" }),
        .symbol = "M",
        .translation = { .factor = 10, .power = 6 }
    });
    EcsGiga = ecs_unit_prefix_init(world, &(ecs_unit_prefix_desc_t){
        .entity = ecs_entity(world, { .name = "Giga" }),
        .symbol = "G",
        .translation = { .factor = 10, .power = 9 }
    });
    EcsTera = ecs_unit_prefix_init(world, &(ecs_unit_prefix_desc_t){
        .entity = ecs_entity(world, { .name = "Tera" }),
        .symbol = "T",
        .translation = { .factor = 10, .power = 12 }
    });
    EcsPeta = ecs_unit_prefix_init(world, &(ecs_unit_prefix_desc_t){
        .entity = ecs_entity(world, { .name = "Peta" }),
        .symbol = "P",
        .translation = { .factor = 10, .power = 15 }
    });
    EcsExa = ecs_unit_prefix_init(world, &(ecs_unit_prefix_desc_t){
        .entity = ecs_entity(world, { .name = "Exa" }),
        .symbol = "E",
        .translation = { .factor = 10, .power = 18 }
    });
    EcsZetta = ecs_unit_prefix_init(world, &(ecs_unit_prefix_desc_t){
        .entity = ecs_entity(world, { .name = "Zetta" }),
        .symbol = "Z",
        .translation = { .factor = 10, .power = 21 }
    });
    EcsYotta = ecs_unit_prefix_init(world, &(ecs_unit_prefix_desc_t){
        .entity = ecs_entity(world, { .name = "Yotta" }),
        .symbol = "Y",
        .translation = { .factor = 10, .power = 24 }
    });

    EcsKibi = ecs_unit_prefix_init(world, &(ecs_unit_prefix_desc_t){
        .entity = ecs_entity(world, { .name = "Kibi" }),
        .symbol = "Ki",
        .translation = { .factor = 1024, .power = 1 }
    });
    EcsMebi = ecs_unit_prefix_init(world, &(ecs_unit_prefix_desc_t){
        .entity = ecs_entity(world, { .name = "Mebi" }),
        .symbol = "Mi",
        .translation = { .factor = 1024, .power = 2 }
    });
    EcsGibi = ecs_unit_prefix_init(world, &(ecs_unit_prefix_desc_t){
        .entity = ecs_entity(world, { .name = "Gibi" }),
        .symbol = "Gi",
        .translation = { .factor = 1024, .power = 3 }
    });
    EcsTebi = ecs_unit_prefix_init(world, &(ecs_unit_prefix_desc_t){
        .entity = ecs_entity(world, { .name = "Tebi" }),
        .symbol = "Ti",
        .translation = { .factor = 1024, .power = 4 }
    });
    EcsPebi = ecs_unit_prefix_init(world, &(ecs_unit_prefix_desc_t){
        .entity = ecs_entity(world, { .name = "Pebi" }),
        .symbol = "Pi",
        .translation = { .factor = 1024, .power = 5 }
    });
    EcsExbi = ecs_unit_prefix_init(world, &(ecs_unit_prefix_desc_t){
        .entity = ecs_entity(world, { .name = "Exbi" }),
        .symbol = "Ei",
        .translation = { .factor = 1024, .power = 6 }
    });
    EcsZebi = ecs_unit_prefix_init(world, &(ecs_unit_prefix_desc_t){
        .entity = ecs_entity(world, { .name = "Zebi" }),
        .symbol = "Zi",
        .translation = { .factor = 1024, .power = 7 }
    });
    EcsYobi = ecs_unit_prefix_init(world, &(ecs_unit_prefix_desc_t){
        .entity = ecs_entity(world, { .name = "Yobi" }),
        .symbol = "Yi",
        .translation = { .factor = 1024, .power = 8 }
    });

    ecs_set_scope(world, prev_scope);

    /* Duration units */

    EcsDuration = ecs_quantity_init(world, &(ecs_entity_desc_t){ 
        .name = "Duration" });
    prev_scope = ecs_set_scope(world, EcsDuration);

        EcsSeconds = ecs_unit_init(world, &(ecs_unit_desc_t){
            .entity = ecs_entity(world, { .name = "Seconds" }),
            .quantity = EcsDuration,
            .symbol = "s" });
        ecs_primitive_init(world, &(ecs_primitive_desc_t){
            .entity = EcsSeconds,
            .kind = EcsF32
        });
            EcsPicoSeconds = ecs_unit_init(world, &(ecs_unit_desc_t){
                .entity = ecs_entity(world, { .name = "PicoSeconds" }),
                .quantity = EcsDuration,
                .base = EcsSeconds,
                .prefix = EcsPico });
            ecs_primitive_init(world, &(ecs_primitive_desc_t){
                .entity = EcsPicoSeconds,
                .kind = EcsF32
            });


            EcsNanoSeconds = ecs_unit_init(world, &(ecs_unit_desc_t){
                .entity = ecs_entity(world, { .name = "NanoSeconds" }),
                .quantity = EcsDuration,
                .base = EcsSeconds,
                .prefix = EcsNano });
            ecs_primitive_init(world, &(ecs_primitive_desc_t){
                .entity = EcsNanoSeconds,
                .kind = EcsF32
            });

            EcsMicroSeconds = ecs_unit_init(world, &(ecs_unit_desc_t){
                .entity = ecs_entity(world, { .name = "MicroSeconds" }),
                .quantity = EcsDuration,
                .base = EcsSeconds,
                .prefix = EcsMicro });
            ecs_primitive_init(world, &(ecs_primitive_desc_t){
                .entity = EcsMicroSeconds,
                .kind = EcsF32
            });

            EcsMilliSeconds = ecs_unit_init(world, &(ecs_unit_desc_t){
                .entity = ecs_entity(world, { .name = "MilliSeconds" }),
                .quantity = EcsDuration,
                .base = EcsSeconds,
                .prefix = EcsMilli });
            ecs_primitive_init(world, &(ecs_primitive_desc_t){
                .entity = EcsMilliSeconds,
                .kind = EcsF32
            });

        EcsMinutes = ecs_unit_init(world, &(ecs_unit_desc_t){
            .entity = ecs_entity(world, { .name = "Minutes" }),
            .quantity = EcsDuration,
            .base = EcsSeconds,
            .symbol = "min",
            .translation = { .factor = 60, .power = 1 } });
        ecs_primitive_init(world, &(ecs_primitive_desc_t){
            .entity = EcsMinutes,
            .kind = EcsU32
        });

        EcsHours = ecs_unit_init(world, &(ecs_unit_desc_t){
            .entity = ecs_entity(world, { .name = "Hours" }),
            .quantity = EcsDuration,
            .base = EcsMinutes,
            .symbol = "h",
            .translation = { .factor = 60, .power = 1 } });
        ecs_primitive_init(world, &(ecs_primitive_desc_t){
            .entity = EcsHours,
            .kind = EcsU32
        });

        EcsDays = ecs_unit_init(world, &(ecs_unit_desc_t){
            .entity = ecs_entity(world, { .name = "Days" }),
            .quantity = EcsDuration,
            .base = EcsHours,
            .symbol = "d",
            .translation = { .factor = 24, .power = 1 } });
        ecs_primitive_init(world, &(ecs_primitive_desc_t){
            .entity = EcsDays,
            .kind = EcsU32
        });
    ecs_set_scope(world, prev_scope);

    /* Time units */

    EcsTime = ecs_quantity_init(world, &(ecs_entity_desc_t){ 
        .name = "Time" });
    prev_scope = ecs_set_scope(world, EcsTime);

        EcsDate = ecs_unit_init(world, &(ecs_unit_desc_t){ 
            .entity = ecs_entity(world, { .name = "Date" }),
            .quantity = EcsTime });
        ecs_primitive_init(world, &(ecs_primitive_desc_t){
            .entity = EcsDate,
            .kind = EcsU32
        });
    ecs_set_scope(world, prev_scope);

    /* Mass units */

    EcsMass = ecs_quantity_init(world, &(ecs_entity_desc_t){ 
        .name = "Mass" });
    prev_scope = ecs_set_scope(world, EcsMass);
        EcsGrams = ecs_unit_init(world, &(ecs_unit_desc_t){ 
            .entity = ecs_entity(world, { .name = "Grams" }),
            .quantity = EcsMass,
            .symbol = "g" });
        ecs_primitive_init(world, &(ecs_primitive_desc_t){
            .entity = EcsGrams,
            .kind = EcsF32
        });
        EcsKiloGrams = ecs_unit_init(world, &(ecs_unit_desc_t){ 
            .entity = ecs_entity(world, { .name = "KiloGrams" }),
            .quantity = EcsMass,
            .prefix = EcsKilo,
            .base = EcsGrams });
        ecs_primitive_init(world, &(ecs_primitive_desc_t){
            .entity = EcsKiloGrams,
            .kind = EcsF32
        });
    ecs_set_scope(world, prev_scope);

    /* Electric current units */

    EcsElectricCurrent = ecs_quantity_init(world, &(ecs_entity_desc_t){ 
        .name = "ElectricCurrent" });
    prev_scope = ecs_set_scope(world, EcsElectricCurrent);
        EcsAmpere = ecs_unit_init(world, &(ecs_unit_desc_t){ 
            .entity = ecs_entity(world, { .name = "Ampere" }),
            .quantity = EcsElectricCurrent,
            .symbol = "A" });
        ecs_primitive_init(world, &(ecs_primitive_desc_t){
            .entity = EcsAmpere,
            .kind = EcsF32
        });
    ecs_set_scope(world, prev_scope);

    /* Amount of substance units */

    EcsAmount = ecs_quantity_init(world, &(ecs_entity_desc_t){ 
        .name = "Amount" });
    prev_scope = ecs_set_scope(world, EcsAmount);
        EcsMole = ecs_unit_init(world, &(ecs_unit_desc_t){ 
            .entity = ecs_entity(world, { .name = "Mole" }),
            .quantity = EcsAmount,
            .symbol = "mol" });
        ecs_primitive_init(world, &(ecs_primitive_desc_t){
            .entity = EcsMole,
            .kind = EcsF32
        });
    ecs_set_scope(world, prev_scope);

    /* Luminous intensity units */

    EcsLuminousIntensity = ecs_quantity_init(world, &(ecs_entity_desc_t){ 
        .name = "LuminousIntensity" });
    prev_scope = ecs_set_scope(world, EcsLuminousIntensity);
        EcsCandela = ecs_unit_init(world, &(ecs_unit_desc_t){ 
            .entity = ecs_entity(world, { .name = "Candela" }),
            .quantity = EcsLuminousIntensity,
            .symbol = "cd" });
        ecs_primitive_init(world, &(ecs_primitive_desc_t){
            .entity = EcsCandela,
            .kind = EcsF32
        });
    ecs_set_scope(world, prev_scope);

    /* Force units */

    EcsForce = ecs_quantity_init(world, &(ecs_entity_desc_t){ 
        .name = "Force" });
    prev_scope = ecs_set_scope(world, EcsForce);
        EcsNewton = ecs_unit_init(world, &(ecs_unit_desc_t){ 
            .entity = ecs_entity(world, { .name = "Newton" }),
            .quantity = EcsForce,
            .symbol = "N" });
        ecs_primitive_init(world, &(ecs_primitive_desc_t){
            .entity = EcsNewton,
            .kind = EcsF32
        });
    ecs_set_scope(world, prev_scope);

    /* Length units */

    EcsLength = ecs_quantity_init(world, &(ecs_entity_desc_t){ 
        .name = "Length" });
    prev_scope = ecs_set_scope(world, EcsLength);
        EcsMeters = ecs_unit_init(world, &(ecs_unit_desc_t){ 
            .entity = ecs_entity(world, { .name = "Meters" }),
            .quantity = EcsLength,
            .symbol = "m" });
        ecs_primitive_init(world, &(ecs_primitive_desc_t){
            .entity = EcsMeters,
            .kind = EcsF32
        });

            EcsPicoMeters = ecs_unit_init(world, &(ecs_unit_desc_t){ 
                .entity = ecs_entity(world, { .name = "PicoMeters" }),
                .quantity = EcsLength,
                .base = EcsMeters,
                .prefix = EcsPico });
            ecs_primitive_init(world, &(ecs_primitive_desc_t){
                .entity = EcsPicoMeters,
                .kind = EcsF32
            });

            EcsNanoMeters = ecs_unit_init(world, &(ecs_unit_desc_t){ 
                .entity = ecs_entity(world, { .name = "NanoMeters" }),
                .quantity = EcsLength,
                .base = EcsMeters,
                .prefix = EcsNano });
            ecs_primitive_init(world, &(ecs_primitive_desc_t){
                .entity = EcsNanoMeters,
                .kind = EcsF32
            });

            EcsMicroMeters = ecs_unit_init(world, &(ecs_unit_desc_t){ 
                .entity = ecs_entity(world, { .name = "MicroMeters" }),
                .quantity = EcsLength,
                .base = EcsMeters,
                .prefix = EcsMicro });
            ecs_primitive_init(world, &(ecs_primitive_desc_t){
                .entity = EcsMicroMeters,
                .kind = EcsF32
            });

            EcsMilliMeters = ecs_unit_init(world, &(ecs_unit_desc_t){ 
                .entity = ecs_entity(world, { .name = "MilliMeters" }),
                .quantity = EcsLength,
                .base = EcsMeters,
                .prefix = EcsMilli });
            ecs_primitive_init(world, &(ecs_primitive_desc_t){
                .entity = EcsMilliMeters,
                .kind = EcsF32
            });

            EcsCentiMeters = ecs_unit_init(world, &(ecs_unit_desc_t){ 
                .entity = ecs_entity(world, { .name = "CentiMeters" }),
                .quantity = EcsLength,
                .base = EcsMeters,
                .prefix = EcsCenti });
            ecs_primitive_init(world, &(ecs_primitive_desc_t){
                .entity = EcsCentiMeters,
                .kind = EcsF32
            });

            EcsKiloMeters = ecs_unit_init(world, &(ecs_unit_desc_t){ 
                .entity = ecs_entity(world, { .name = "KiloMeters" }),
                .quantity = EcsLength,
                .base = EcsMeters,
                .prefix = EcsKilo });
            ecs_primitive_init(world, &(ecs_primitive_desc_t){
                .entity = EcsKiloMeters,
                .kind = EcsF32
            });
            
        EcsMiles = ecs_unit_init(world, &(ecs_unit_desc_t){ 
            .entity = ecs_entity(world, { .name = "Miles" }),
            .quantity = EcsLength,
            .symbol = "mi"
        });
        ecs_primitive_init(world, &(ecs_primitive_desc_t){
            .entity = EcsMiles,
            .kind = EcsF32
        });

        EcsPixels = ecs_unit_init(world, &(ecs_unit_desc_t){ 
            .entity = ecs_entity(world, { .name = "Pixels" }),
            .quantity = EcsLength,
            .symbol = "px"
        });
        ecs_primitive_init(world, &(ecs_primitive_desc_t){
            .entity = EcsPixels,
            .kind = EcsF32
        });
    ecs_set_scope(world, prev_scope);

    /* Pressure units */

    EcsPressure = ecs_quantity_init(world, &(ecs_entity_desc_t){ 
        .name = "Pressure" });
    prev_scope = ecs_set_scope(world, EcsPressure);
        EcsPascal = ecs_unit_init(world, &(ecs_unit_desc_t){ 
            .entity = ecs_entity(world, { .name = "Pascal" }),
            .quantity = EcsPressure,
            .symbol = "Pa" });
        ecs_primitive_init(world, &(ecs_primitive_desc_t){
            .entity = EcsPascal,
            .kind = EcsF32
        });
        EcsBar = ecs_unit_init(world, &(ecs_unit_desc_t){ 
            .entity = ecs_entity(world, { .name = "Bar" }),
            .quantity = EcsPressure,
            .symbol = "bar" });
        ecs_primitive_init(world, &(ecs_primitive_desc_t){
            .entity = EcsBar,
            .kind = EcsF32
        });
    ecs_set_scope(world, prev_scope);

    /* Speed units */

    EcsSpeed = ecs_quantity_init(world, &(ecs_entity_desc_t){ 
        .name = "Speed" });
    prev_scope = ecs_set_scope(world, EcsSpeed);
        EcsMetersPerSecond = ecs_unit_init(world, &(ecs_unit_desc_t){ 
            .entity = ecs_entity(world, { .name = "MetersPerSecond" }),
            .quantity = EcsSpeed,
            .base = EcsMeters,
            .over = EcsSeconds });
        ecs_primitive_init(world, &(ecs_primitive_desc_t){
            .entity = EcsMetersPerSecond,
            .kind = EcsF32
        });
        EcsKiloMetersPerSecond = ecs_unit_init(world, &(ecs_unit_desc_t){ 
            .entity = ecs_entity(world, { .name = "KiloMetersPerSecond" }),
            .quantity = EcsSpeed,
            .base = EcsKiloMeters,
            .over = EcsSeconds });
        ecs_primitive_init(world, &(ecs_primitive_desc_t){
            .entity = EcsKiloMetersPerSecond,
            .kind = EcsF32
        });
        EcsKiloMetersPerHour = ecs_unit_init(world, &(ecs_unit_desc_t){ 
            .entity = ecs_entity(world, { .name = "KiloMetersPerHour" }),
            .quantity = EcsSpeed,
            .base = EcsKiloMeters,
            .over = EcsHours });
        ecs_primitive_init(world, &(ecs_primitive_desc_t){
            .entity = EcsKiloMetersPerHour,
            .kind = EcsF32
        });
        EcsMilesPerHour = ecs_unit_init(world, &(ecs_unit_desc_t){ 
            .entity = ecs_entity(world, { .name = "MilesPerHour" }),
            .quantity = EcsSpeed,
            .base = EcsMiles,
            .over = EcsHours });
        ecs_primitive_init(world, &(ecs_primitive_desc_t){
            .entity = EcsMilesPerHour,
            .kind = EcsF32
        });
    ecs_set_scope(world, prev_scope);
    
    /* Acceleration */

    EcsAcceleration = ecs_unit_init(world, &(ecs_unit_desc_t){ 
        .entity = ecs_entity(world, { .name = "Acceleration" }),
        .base = EcsMetersPerSecond,
        .over = EcsSeconds });
    ecs_quantity_init(world, &(ecs_entity_desc_t){
        .id = EcsAcceleration
    });
    ecs_primitive_init(world, &(ecs_primitive_desc_t){
        .entity = EcsAcceleration,
        .kind = EcsF32
    });

    /* Temperature units */

    EcsTemperature = ecs_quantity_init(world, &(ecs_entity_desc_t){ 
        .name = "Temperature" });
    prev_scope = ecs_set_scope(world, EcsTemperature);
        EcsKelvin = ecs_unit_init(world, &(ecs_unit_desc_t){ 
            .entity = ecs_entity(world, { .name = "Kelvin" }),
            .quantity = EcsTemperature,
            .symbol = "K" });
        ecs_primitive_init(world, &(ecs_primitive_desc_t){
            .entity = EcsKelvin,
            .kind = EcsF32
        });
        EcsCelsius = ecs_unit_init(world, &(ecs_unit_desc_t){ 
            .entity = ecs_entity(world, { .name = "Celsius" }),
            .quantity = EcsTemperature,
            .symbol = "°C" });
        ecs_primitive_init(world, &(ecs_primitive_desc_t){
            .entity = EcsCelsius,
            .kind = EcsF32
        });
        EcsFahrenheit = ecs_unit_init(world, &(ecs_unit_desc_t){ 
            .entity = ecs_entity(world, { .name = "Fahrenheit" }),
            .quantity = EcsTemperature,
            .symbol = "F" });
        ecs_primitive_init(world, &(ecs_primitive_desc_t){
            .entity = EcsFahrenheit,
            .kind = EcsF32
        });
    ecs_set_scope(world, prev_scope);

    /* Data units */

    EcsData = ecs_quantity_init(world, &(ecs_entity_desc_t){ 
        .name = "Data" });
    prev_scope = ecs_set_scope(world, EcsData);

        EcsBits = ecs_unit_init(world, &(ecs_unit_desc_t){ 
            .entity = ecs_entity(world, { .name = "Bits" }),
            .quantity = EcsData,
            .symbol = "bit" });
        ecs_primitive_init(world, &(ecs_primitive_desc_t){
            .entity = EcsBits,
            .kind = EcsU64
        });

            EcsKiloBits = ecs_unit_init(world, &(ecs_unit_desc_t){ 
                .entity = ecs_entity(world, { .name = "KiloBits" }),
                .quantity = EcsData,
                .base = EcsBits,
                .prefix = EcsKilo });
            ecs_primitive_init(world, &(ecs_primitive_desc_t){
                .entity = EcsKiloBits,
                .kind = EcsU64
            });

            EcsMegaBits = ecs_unit_init(world, &(ecs_unit_desc_t){ 
                .entity = ecs_entity(world, { .name = "MegaBits" }),
                .quantity = EcsData,
                .base = EcsBits,
                .prefix = EcsMega });
            ecs_primitive_init(world, &(ecs_primitive_desc_t){
                .entity = EcsMegaBits,
                .kind = EcsU64
            });

            EcsGigaBits = ecs_unit_init(world, &(ecs_unit_desc_t){ 
                .entity = ecs_entity(world, { .name = "GigaBits" }),
                .quantity = EcsData,
                .base = EcsBits,
                .prefix = EcsGiga });
            ecs_primitive_init(world, &(ecs_primitive_desc_t){
                .entity = EcsGigaBits,
                .kind = EcsU64
            });

        EcsBytes = ecs_unit_init(world, &(ecs_unit_desc_t){ 
            .entity = ecs_entity(world, { .name = "Bytes" }),
            .quantity = EcsData,
            .symbol = "B",
            .base = EcsBits,
            .translation = { .factor = 8, .power = 1 } });
        ecs_primitive_init(world, &(ecs_primitive_desc_t){
            .entity = EcsBytes,
            .kind = EcsU64
        });

            EcsKiloBytes = ecs_unit_init(world, &(ecs_unit_desc_t){ 
                .entity = ecs_entity(world, { .name = "KiloBytes" }),
                .quantity = EcsData,
                .base = EcsBytes,
                .prefix = EcsKilo });
            ecs_primitive_init(world, &(ecs_primitive_desc_t){
                .entity = EcsKiloBytes,
                .kind = EcsU64
            });

            EcsMegaBytes = ecs_unit_init(world, &(ecs_unit_desc_t){ 
                .entity = ecs_entity(world, { .name = "MegaBytes" }),
                .quantity = EcsData,
                .base = EcsBytes,
                .prefix = EcsMega });
            ecs_primitive_init(world, &(ecs_primitive_desc_t){
                .entity = EcsMegaBytes,
                .kind = EcsU64
            });

            EcsGigaBytes = ecs_unit_init(world, &(ecs_unit_desc_t){ 
                .entity = ecs_entity(world, { .name = "GigaBytes" }),
                .quantity = EcsData,
                .base = EcsBytes,
                .prefix = EcsGiga });
            ecs_primitive_init(world, &(ecs_primitive_desc_t){
                .entity = EcsGigaBytes,
                .kind = EcsU64
            });

            EcsKibiBytes = ecs_unit_init(world, &(ecs_unit_desc_t){ 
                .entity = ecs_entity(world, { .name = "KibiBytes" }),
                .quantity = EcsData,
                .base = EcsBytes,
                .prefix = EcsKibi });
            ecs_primitive_init(world, &(ecs_primitive_desc_t){
                .entity = EcsKibiBytes,
                .kind = EcsU64
            });

            EcsMebiBytes = ecs_unit_init(world, &(ecs_unit_desc_t){ 
                .entity = ecs_entity(world, { .name = "MebiBytes" }),
                .quantity = EcsData,
                .base = EcsBytes,
                .prefix = EcsMebi });
            ecs_primitive_init(world, &(ecs_primitive_desc_t){
                .entity = EcsMebiBytes,
                .kind = EcsU64
            });

            EcsGibiBytes = ecs_unit_init(world, &(ecs_unit_desc_t){ 
                .entity = ecs_entity(world, { .name = "GibiBytes" }),
                .quantity = EcsData,
                .base = EcsBytes,
                .prefix = EcsGibi });
            ecs_primitive_init(world, &(ecs_primitive_desc_t){
                .entity = EcsGibiBytes,
                .kind = EcsU64
            });

    ecs_set_scope(world, prev_scope);

    /* DataRate units */

    EcsDataRate = ecs_quantity_init(world, &(ecs_entity_desc_t){ 
        .name = "DataRate" });
    prev_scope = ecs_set_scope(world, EcsDataRate);

        EcsBitsPerSecond = ecs_unit_init(world, &(ecs_unit_desc_t){ 
            .entity = ecs_entity(world, { .name = "BitsPerSecond" }),
            .quantity = EcsDataRate,
            .base = EcsBits,
            .over = EcsSeconds });
        ecs_primitive_init(world, &(ecs_primitive_desc_t){
            .entity = EcsBitsPerSecond,
            .kind = EcsU64
        });

            EcsKiloBitsPerSecond = ecs_unit_init(world, &(ecs_unit_desc_t){ 
                .entity = ecs_entity(world, { .name = "KiloBitsPerSecond" }),
                .quantity = EcsDataRate,
                .base = EcsKiloBits,
                .over = EcsSeconds
            });
            ecs_primitive_init(world, &(ecs_primitive_desc_t){
                .entity = EcsKiloBitsPerSecond,
                .kind = EcsU64
            });

            EcsMegaBitsPerSecond = ecs_unit_init(world, &(ecs_unit_desc_t){ 
                .entity = ecs_entity(world, { .name = "MegaBitsPerSecond" }),
                .quantity = EcsDataRate,
                .base = EcsMegaBits,
                .over = EcsSeconds
            });
            ecs_primitive_init(world, &(ecs_primitive_desc_t){
                .entity = EcsMegaBitsPerSecond,
                .kind = EcsU64
            });

            EcsGigaBitsPerSecond = ecs_unit_init(world, &(ecs_unit_desc_t){ 
                .entity = ecs_entity(world, { .name = "GigaBitsPerSecond" }),
                .quantity = EcsDataRate,
                .base = EcsGigaBits,
                .over = EcsSeconds
            });
            ecs_primitive_init(world, &(ecs_primitive_desc_t){
                .entity = EcsGigaBitsPerSecond,
                .kind = EcsU64
            });

        EcsBytesPerSecond = ecs_unit_init(world, &(ecs_unit_desc_t){ 
            .entity = ecs_entity(world, { .name = "BytesPerSecond" }),
            .quantity = EcsDataRate,
            .base = EcsBytes,
            .over = EcsSeconds });
        ecs_primitive_init(world, &(ecs_primitive_desc_t){
            .entity = EcsBytesPerSecond,
            .kind = EcsU64
        });

            EcsKiloBytesPerSecond = ecs_unit_init(world, &(ecs_unit_desc_t){ 
                .entity = ecs_entity(world, { .name = "KiloBytesPerSecond" }),
                .quantity = EcsDataRate,
                .base = EcsKiloBytes,
                .over = EcsSeconds
            });
            ecs_primitive_init(world, &(ecs_primitive_desc_t){
                .entity = EcsKiloBytesPerSecond,
                .kind = EcsU64
            });

            EcsMegaBytesPerSecond = ecs_unit_init(world, &(ecs_unit_desc_t){ 
                .entity = ecs_entity(world, { .name = "MegaBytesPerSecond" }),
                .quantity = EcsDataRate,
                .base = EcsMegaBytes,
                .over = EcsSeconds
            });
            ecs_primitive_init(world, &(ecs_primitive_desc_t){
                .entity = EcsMegaBytesPerSecond,
                .kind = EcsU64
            });

            EcsGigaBytesPerSecond = ecs_unit_init(world, &(ecs_unit_desc_t){ 
                .entity = ecs_entity(world, { .name = "GigaBytesPerSecond" }),
                .quantity = EcsDataRate,
                .base = EcsGigaBytes,
                .over = EcsSeconds
            });
            ecs_primitive_init(world, &(ecs_primitive_desc_t){
                .entity = EcsGigaBytesPerSecond,
                .kind = EcsU64
            });

        ecs_set_scope(world, prev_scope);

    /* Percentage */

    EcsPercentage = ecs_quantity_init(world, &(ecs_entity_desc_t){ 
        .name = "Percentage" });
    ecs_unit_init(world, &(ecs_unit_desc_t){ 
        .entity = EcsPercentage,
        .symbol = "%"
    });
    ecs_primitive_init(world, &(ecs_primitive_desc_t){
        .entity = EcsPercentage,
        .kind = EcsF32
    });

    /* Angles */

    EcsAngle = ecs_quantity_init(world, &(ecs_entity_desc_t){ 
        .name = "Angle" });
    prev_scope = ecs_set_scope(world, EcsAngle);
        EcsRadians = ecs_unit_init(world, &(ecs_unit_desc_t){ 
            .entity = ecs_entity(world, { .name = "Radians" }),
            .quantity = EcsAngle,
            .symbol = "rad" });
        ecs_primitive_init(world, &(ecs_primitive_desc_t){
            .entity = EcsRadians,
            .kind = EcsF32
        });

        EcsDegrees = ecs_unit_init(world, &(ecs_unit_desc_t){ 
            .entity = ecs_entity(world, { .name = "Degrees" }),
            .quantity = EcsAngle,
            .symbol = "°" });
        ecs_primitive_init(world, &(ecs_primitive_desc_t){
            .entity = EcsDegrees,
            .kind = EcsF32
        });
    ecs_set_scope(world, prev_scope);

    /* DeciBel */

    EcsBel = ecs_unit_init(world, &(ecs_unit_desc_t){ 
        .entity = ecs_entity(world, { .name = "Bel" }),
        .symbol = "B" });
    ecs_primitive_init(world, &(ecs_primitive_desc_t){
        .entity = EcsBel,
        .kind = EcsF32
    });
    EcsDeciBel = ecs_unit_init(world, &(ecs_unit_desc_t){ 
        .entity = ecs_entity(world, { .name = "DeciBel" }),
        .prefix = EcsDeci,
        .base = EcsBel });
    ecs_primitive_init(world, &(ecs_primitive_desc_t){
        .entity = EcsDeciBel,
        .kind = EcsF32
    });

    EcsFrequency = ecs_quantity_init(world, &(ecs_entity_desc_t){ 
        .name = "Frequency" });
    prev_scope = ecs_set_scope(world, EcsFrequency);

        EcsHertz = ecs_unit_init(world, &(ecs_unit_desc_t){ 
            .entity = ecs_entity(world, { .name = "Hertz" }),
            .quantity = EcsFrequency,
            .symbol = "Hz" });
        ecs_primitive_init(world, &(ecs_primitive_desc_t){
            .entity = EcsHertz,
            .kind = EcsF32
        });

        EcsKiloHertz = ecs_unit_init(world, &(ecs_unit_desc_t){ 
            .entity = ecs_entity(world, { .name = "KiloHertz" }),
            .prefix = EcsKilo,
            .base = EcsHertz });
        ecs_primitive_init(world, &(ecs_primitive_desc_t){
            .entity = EcsKiloHertz,
            .kind = EcsF32
        });

        EcsMegaHertz = ecs_unit_init(world, &(ecs_unit_desc_t){ 
            .entity = ecs_entity(world, { .name = "MegaHertz" }),
            .prefix = EcsMega,
            .base = EcsHertz });
        ecs_primitive_init(world, &(ecs_primitive_desc_t){
            .entity = EcsMegaHertz,
            .kind = EcsF32
        });

        EcsGigaHertz = ecs_unit_init(world, &(ecs_unit_desc_t){ 
            .entity = ecs_entity(world, { .name = "GigaHertz" }),
            .prefix = EcsGiga,
            .base = EcsHertz });
        ecs_primitive_init(world, &(ecs_primitive_desc_t){
            .entity = EcsGigaHertz,
            .kind = EcsF32
        });
    ecs_set_scope(world, prev_scope);

    EcsUri = ecs_quantity_init(world, &(ecs_entity_desc_t){ 
        .name = "Uri" });
    prev_scope = ecs_set_scope(world, EcsUri);

        EcsUriHyperlink = ecs_unit_init(world, &(ecs_unit_desc_t){ 
            .entity = ecs_entity(world, { .name = "Hyperlink" }),
            .quantity = EcsUri });
        ecs_primitive_init(world, &(ecs_primitive_desc_t){
            .entity = EcsUriHyperlink,
            .kind = EcsString
        });

        EcsUriImage = ecs_unit_init(world, &(ecs_unit_desc_t){ 
            .entity = ecs_entity(world, { .name = "Image" }),
            .quantity = EcsUri });
        ecs_primitive_init(world, &(ecs_primitive_desc_t){
            .entity = EcsUriImage,
            .kind = EcsString
        });

        EcsUriFile = ecs_unit_init(world, &(ecs_unit_desc_t){ 
            .entity = ecs_entity(world, { .name = "File" }),
            .quantity = EcsUri });
        ecs_primitive_init(world, &(ecs_primitive_desc_t){
            .entity = EcsUriFile,
            .kind = EcsString
        });
    ecs_set_scope(world, prev_scope);

    /* Documentation */
#ifdef FLECS_DOC
    ECS_IMPORT(world, FlecsDoc);

    ecs_doc_set_brief(world, EcsDuration, 
        "Time amount (e.g. \"20 seconds\", \"2 hours\")");
    ecs_doc_set_brief(world, EcsSeconds, "Time amount in seconds");
    ecs_doc_set_brief(world, EcsMinutes, "60 seconds");
    ecs_doc_set_brief(world, EcsHours, "60 minutes");
    ecs_doc_set_brief(world, EcsDays, "24 hours");

    ecs_doc_set_brief(world, EcsTime,
        "Time passed since an epoch (e.g. \"5pm\", \"March 3rd 2022\")");
    ecs_doc_set_brief(world, EcsDate,
        "Seconds passed since January 1st 1970");

    ecs_doc_set_brief(world, EcsMass, "Units of mass (e.g. \"5 kilograms\")");

    ecs_doc_set_brief(world, EcsElectricCurrent,
        "Units of electrical current (e.g. \"2 ampere\")");

    ecs_doc_set_brief(world, EcsAmount,
        "Units of amount of substance (e.g. \"2 mole\")");

    ecs_doc_set_brief(world, EcsLuminousIntensity,
        "Units of luminous intensity (e.g. \"1 candela\")");

    ecs_doc_set_brief(world, EcsForce, "Units of force (e.g. \"10 newton\")");

    ecs_doc_set_brief(world, EcsLength,
        "Units of length (e.g. \"5 meters\", \"20 miles\")");

    ecs_doc_set_brief(world, EcsPressure, 
        "Units of pressure (e.g. \"1 bar\", \"1000 pascal\")");

    ecs_doc_set_brief(world, EcsSpeed,
        "Units of movement (e.g. \"5 meters/second\")");

    ecs_doc_set_brief(world, EcsAcceleration,
        "Unit of speed increase (e.g. \"5 meters/second/second\")");

    ecs_doc_set_brief(world, EcsTemperature,
        "Units of temperature (e.g. \"5 degrees Celsius\")");

    ecs_doc_set_brief(world, EcsData,
        "Units of information (e.g. \"8 bits\", \"100 megabytes\")");

    ecs_doc_set_brief(world, EcsDataRate,
        "Units of data transmission (e.g. \"100 megabits/second\")");

    ecs_doc_set_brief(world, EcsAngle,
        "Units of rotation (e.g. \"1.2 radians\", \"180 degrees\")");

    ecs_doc_set_brief(world, EcsFrequency, 
        "The number of occurrences of a repeating event per unit of time.");

    ecs_doc_set_brief(world, EcsUri, "Universal resource identifier.");
#endif
}

#endif

/**
 * @file addons/snapshot.c
 * @brief Snapshot addon.
 */


#ifdef FLECS_SNAPSHOT


/* World snapshot */
struct ecs_snapshot_t {
    ecs_world_t *world;
    ecs_entity_index_t entity_index;
    ecs_vec_t tables;
    uint64_t last_id;
};

/** Small footprint data structure for storing data associated with a table. */
typedef struct ecs_table_leaf_t {
    ecs_table_t *table;
    ecs_type_t type;
    ecs_data_t *data;
} ecs_table_leaf_t;

static
ecs_data_t* flecs_duplicate_data(
    ecs_world_t *world,
    ecs_table_t *table,
    ecs_data_t *main_data)
{
    if (!ecs_table_count(table)) {
        return NULL;
    }

    ecs_data_t *result = ecs_os_calloc_t(ecs_data_t);
    int32_t i, column_count = table->storage_count;
    result->columns = flecs_wdup_n(world, ecs_vec_t, column_count, 
        main_data->columns);

    /* Copy entities and records */
    ecs_allocator_t *a = &world->allocator;
    result->entities = ecs_vec_copy_t(a, &main_data->entities, ecs_entity_t);
    result->records = ecs_vec_copy_t(a, &main_data->records, ecs_record_t*);

    /* Copy each column */
    for (i = 0; i < column_count; i ++) {
        ecs_vec_t *column = &result->columns[i];
        ecs_type_info_t *ti = table->type_info[i];
        int32_t size = ti->size;
        ecs_copy_t copy = ti->hooks.copy;
        if (copy) {
            ecs_vec_t dst = ecs_vec_copy(a, column, size);
            int32_t count = ecs_vec_count(column);
            void *dst_ptr = ecs_vec_first(&dst);
            void *src_ptr = ecs_vec_first(column);

            ecs_xtor_t ctor = ti->hooks.ctor;
            if (ctor) {
                ctor(dst_ptr, count, ti);
            }

            copy(dst_ptr, src_ptr, count, ti);
            *column = dst;
        } else {
            *column = ecs_vec_copy(a, column, size);
        }
    }

    return result;
}

static
void snapshot_table(
    const ecs_world_t *world,
    ecs_snapshot_t *snapshot,
    ecs_table_t *table)
{
    if (table->flags & EcsTableHasBuiltins) {
        return;
    }
    
    ecs_table_leaf_t *l = ecs_vec_get_t(
        &snapshot->tables, ecs_table_leaf_t, (int32_t)table->id);
    ecs_assert(l != NULL, ECS_INTERNAL_ERROR, NULL);
    
    l->table = table;
    l->type = flecs_type_copy((ecs_world_t*)world, &table->type);
    l->data = flecs_duplicate_data((ecs_world_t*)world, table, &table->data);
}

static
ecs_snapshot_t* snapshot_create(
    const ecs_world_t *world,
    const ecs_entity_index_t *entity_index,
    ecs_iter_t *iter,
    ecs_iter_next_action_t next)
{
    ecs_snapshot_t *result = ecs_os_calloc_t(ecs_snapshot_t);
    ecs_assert(result != NULL, ECS_OUT_OF_MEMORY, NULL);

    ecs_run_aperiodic((ecs_world_t*)world, 0);

    result->world = (ecs_world_t*)world;

    /* If no iterator is provided, the snapshot will be taken of the entire
     * world, and we can simply copy the entity index as it will be restored
     * entirely upon snapshote restore. */
    if (!iter && entity_index) {
        flecs_entities_copy(&result->entity_index, entity_index);
    }

    /* Create vector with as many elements as tables, so we can store the
     * snapshot tables at their element ids. When restoring a snapshot, the code
     * will run a diff between the tables in the world and the snapshot, to see
     * which of the world tables still exist, no longer exist, or need to be
     * deleted. */
    uint64_t t, table_count = flecs_sparse_last_id(&world->store.tables) + 1;
    ecs_vec_init_t(NULL, &result->tables, ecs_table_leaf_t, (int32_t)table_count);
    ecs_vec_set_count_t(NULL, &result->tables, ecs_table_leaf_t, (int32_t)table_count);
    ecs_table_leaf_t *arr = ecs_vec_first_t(&result->tables, ecs_table_leaf_t);

    /* Array may have holes, so initialize with 0 */
    ecs_os_memset_n(arr, 0, ecs_table_leaf_t, table_count);

    /* Iterate tables in iterator */
    if (iter) {
        while (next(iter)) {
            ecs_table_t *table = iter->table;
            snapshot_table(world, result, table);
        }
    } else {
        for (t = 1; t < table_count; t ++) {
            ecs_table_t *table = flecs_sparse_get_t(
                &world->store.tables, ecs_table_t, t);
            snapshot_table(world, result, table);
        }
    }

    return result;
}

/** Create a snapshot */
ecs_snapshot_t* ecs_snapshot_take(
    ecs_world_t *stage)
{
    const ecs_world_t *world = ecs_get_world(stage);

    ecs_snapshot_t *result = snapshot_create(
        world, ecs_eis(world), NULL, NULL);

    result->last_id = flecs_entities_max_id(world);

    return result;
}

/** Create a filtered snapshot */
ecs_snapshot_t* ecs_snapshot_take_w_iter(
    ecs_iter_t *iter)
{
    ecs_world_t *world = iter->world;
    ecs_assert(world != NULL, ECS_INTERNAL_ERROR, NULL);

    ecs_snapshot_t *result = snapshot_create(
        world, ecs_eis(world), iter, iter ? iter->next : NULL);

    result->last_id = flecs_entities_max_id(world);

    return result;
}

/* Restoring an unfiltered snapshot restores the world to the exact state it was
 * when the snapshot was taken. */
static
void restore_unfiltered(
    ecs_world_t *world,
    ecs_snapshot_t *snapshot)
{
    flecs_entity_index_restore(ecs_eis(world), &snapshot->entity_index);
    flecs_entity_index_fini(&snapshot->entity_index);
    
    flecs_entities_max_id(world) = snapshot->last_id;

    ecs_table_leaf_t *leafs = ecs_vec_first_t(&snapshot->tables, ecs_table_leaf_t);
    int32_t i, count = (int32_t)flecs_sparse_last_id(&world->store.tables);
    int32_t snapshot_count = ecs_vec_count(&snapshot->tables);

    for (i = 1; i <= count; i ++) {
        ecs_table_t *world_table = flecs_sparse_get_t(
            &world->store.tables, ecs_table_t, (uint32_t)i);

        if (world_table && (world_table->flags & EcsTableHasBuiltins)) {
            continue;
        }

        ecs_table_leaf_t *snapshot_table = NULL;
        if (i < snapshot_count) {
            snapshot_table = &leafs[i];
            if (!snapshot_table->table) {
                snapshot_table = NULL;
            }
        }

        /* If the world table no longer exists but the snapshot table does,
         * reinsert it */
        if (!world_table && snapshot_table) {
            ecs_table_t *table = flecs_table_find_or_create(world, 
                &snapshot_table->type);
            ecs_assert(table != NULL, ECS_INTERNAL_ERROR, NULL);

            if (snapshot_table->data) {
                flecs_table_replace_data(world, table, snapshot_table->data);
            }
        
        /* If the world table still exists, replace its data */
        } else if (world_table && snapshot_table) {
            ecs_assert(snapshot_table->table == world_table, 
                ECS_INTERNAL_ERROR, NULL);

            if (snapshot_table->data) {
                flecs_table_replace_data(
                    world, world_table, snapshot_table->data);
            } else {
                flecs_table_clear_data(
                    world, world_table, &world_table->data);
                flecs_table_init_data(world, world_table);
            }
        
        /* If the snapshot table doesn't exist, this table was created after the
         * snapshot was taken and needs to be deleted */
        } else if (world_table && !snapshot_table) {
            /* Deleting a table invokes OnRemove triggers & updates the entity
             * index. That is not what we want, since entities may no longer be
             * valid (if they don't exist in the snapshot) or may have been
             * restored in a different table. Therefore first clear the data
             * from the table (which doesn't invoke triggers), and then delete
             * the table. */
            flecs_table_clear_data(world, world_table, &world_table->data);
            flecs_delete_table(world, world_table);
        
        /* If there is no world & snapshot table, nothing needs to be done */
        } else { }

        if (snapshot_table) {
            ecs_os_free(snapshot_table->data);
            flecs_type_free(world, &snapshot_table->type);
        }
    }

    /* Now that all tables have been restored and world is in a consistent
     * state, run OnSet systems */
    int32_t world_count = flecs_sparse_count(&world->store.tables);
    for (i = 0; i < world_count; i ++) {
        ecs_table_t *table = flecs_sparse_get_dense_t(
            &world->store.tables, ecs_table_t, i);
        if (table->flags & EcsTableHasBuiltins) {
            continue;
        }

        int32_t tcount = ecs_table_count(table);
        if (tcount) {
            flecs_notify_on_set(world, table, 0, tcount, NULL, true);
        }
    }
}

/* Restoring a filtered snapshots only restores the entities in the snapshot
 * to their previous state. */
static
void restore_filtered(
    ecs_world_t *world,
    ecs_snapshot_t *snapshot)
{
    ecs_table_leaf_t *leafs = ecs_vec_first_t(&snapshot->tables, ecs_table_leaf_t);
    int32_t l = 0, snapshot_count = ecs_vec_count(&snapshot->tables);

    for (l = 0; l < snapshot_count; l ++) {
        ecs_table_leaf_t *snapshot_table = &leafs[l];
        ecs_table_t *table = snapshot_table->table;

        if (!table) {
            continue;
        }

        ecs_data_t *data = snapshot_table->data;
        if (!data) {
            flecs_type_free(world, &snapshot_table->type);
            continue;
        }

        /* Delete entity from storage first, so that when we restore it to the
         * current table we can be sure that there won't be any duplicates */
        int32_t i, entity_count = ecs_vec_count(&data->entities);
        ecs_entity_t *entities = ecs_vec_first(
            &snapshot_table->data->entities);
        for (i = 0; i < entity_count; i ++) {
            ecs_entity_t e = entities[i];
            ecs_record_t *r = flecs_entities_try(world, e);
            if (r && r->table) {
                flecs_table_delete(world, r->table, 
                    ECS_RECORD_TO_ROW(r->row), true);
            } else {
                /* Make sure that the entity has the same generation count */
                flecs_entities_set_generation(world, e);
            }
        }

        /* Merge data from snapshot table with world table */
        int32_t old_count = ecs_table_count(snapshot_table->table);
        int32_t new_count = flecs_table_data_count(snapshot_table->data);

        flecs_table_merge(world, table, table, &table->data, snapshot_table->data);

        /* Run OnSet systems for merged entities */
        if (new_count) {
            flecs_notify_on_set(
                world, table, old_count, new_count, NULL, true);
        }

        flecs_wfree_n(world, ecs_vec_t, table->storage_count,
            snapshot_table->data->columns);
        ecs_os_free(snapshot_table->data);
        flecs_type_free(world, &snapshot_table->type);
    }
}

/** Restore a snapshot */
void ecs_snapshot_restore(
    ecs_world_t *world,
    ecs_snapshot_t *snapshot)
{
    ecs_run_aperiodic(world, 0);
    
    if (flecs_entity_index_count(&snapshot->entity_index) > 0) {
        /* Unfiltered snapshots have a copy of the entity index which is
         * copied back entirely when the snapshot is restored */
        restore_unfiltered(world, snapshot);
    } else {
        restore_filtered(world, snapshot);
    }

    ecs_vec_fini_t(NULL, &snapshot->tables, ecs_table_leaf_t);

    ecs_os_free(snapshot);
}

ecs_iter_t ecs_snapshot_iter(
    ecs_snapshot_t *snapshot)
{
    ecs_snapshot_iter_t iter = {
        .tables = snapshot->tables,
        .index = 0
    };

    return (ecs_iter_t){
        .world = snapshot->world,
        .table_count = ecs_vec_count(&snapshot->tables),
        .priv.iter.snapshot = iter,
        .next = ecs_snapshot_next
    };
}

bool ecs_snapshot_next(
    ecs_iter_t *it)
{
    ecs_snapshot_iter_t *iter = &it->priv.iter.snapshot;
    ecs_table_leaf_t *tables = ecs_vec_first_t(&iter->tables, ecs_table_leaf_t);
    int32_t count = ecs_vec_count(&iter->tables);
    int32_t i;

    for (i = iter->index; i < count; i ++) {
        ecs_table_t *table = tables[i].table;
        if (!table) {
            continue;
        }

        ecs_data_t *data = tables[i].data;

        it->table = table;
        it->count = ecs_table_count(table);
        if (data) {
            it->entities = ecs_vec_first(&data->entities);
        } else {
            it->entities = NULL;
        }

        ECS_BIT_SET(it->flags, EcsIterIsValid);
        iter->index = i + 1;
        
        goto yield;
    }

    ECS_BIT_CLEAR(it->flags, EcsIterIsValid);
    return false;

yield:
    ECS_BIT_CLEAR(it->flags, EcsIterIsValid);
    return true;    
}

/** Cleanup snapshot */
void ecs_snapshot_free(
    ecs_snapshot_t *snapshot)
{
    flecs_entity_index_fini(&snapshot->entity_index);

    ecs_table_leaf_t *tables = ecs_vec_first_t(&snapshot->tables, ecs_table_leaf_t);
    int32_t i, count = ecs_vec_count(&snapshot->tables);
    for (i = 0; i < count; i ++) {
        ecs_table_leaf_t *snapshot_table = &tables[i];
        ecs_table_t *table = snapshot_table->table;
        if (table) {
            ecs_data_t *data = snapshot_table->data;
            if (data) {
                flecs_table_clear_data(snapshot->world, table, data);
                ecs_os_free(data);
            }
            flecs_type_free(snapshot->world, &snapshot_table->type);
        }
    }    

    ecs_vec_fini_t(NULL, &snapshot->tables, ecs_table_leaf_t);
    ecs_os_free(snapshot);
}

#endif

/**
 * @file addons/system/system.c
 * @brief System addon.
 */


#ifdef FLECS_SYSTEM


ecs_mixins_t ecs_system_t_mixins = {
    .type_name = "ecs_system_t",
    .elems = {
        [EcsMixinWorld] = offsetof(ecs_system_t, world),
        [EcsMixinEntity] = offsetof(ecs_system_t, entity),
        [EcsMixinDtor] = offsetof(ecs_system_t, dtor)
    }
};

/* -- Public API -- */

ecs_entity_t ecs_run_intern(
    ecs_world_t *world,
    ecs_stage_t *stage,
    ecs_entity_t system,
    ecs_system_t *system_data,
    int32_t stage_index,
    int32_t stage_count,    
    ecs_ftime_t delta_time,
    int32_t offset,
    int32_t limit,
    void *param) 
{
    ecs_ftime_t time_elapsed = delta_time;
    ecs_entity_t tick_source = system_data->tick_source;

    /* Support legacy behavior */
    if (!param) {
        param = system_data->ctx;
    }

    if (tick_source) {
        const EcsTickSource *tick = ecs_get(
            world, tick_source, EcsTickSource);

        if (tick) {
            time_elapsed = tick->time_elapsed;

            /* If timer hasn't fired we shouldn't run the system */
            if (!tick->tick) {
                return 0;
            }
        } else {
            /* If a timer has been set but the timer entity does not have the
             * EcsTimer component, don't run the system. This can be the result
             * of a single-shot timer that has fired already. Not resetting the
             * timer field of the system will ensure that the system won't be
             * ran after the timer has fired. */
            return 0;
        }
    }

    if (ecs_should_log_3()) {
        char *path = ecs_get_fullpath(world, system);
        ecs_dbg_3("worker %d: %s", stage_index, path);
        ecs_os_free(path);
    }

    ecs_time_t time_start;
    bool measure_time = ECS_BIT_IS_SET(world->flags, EcsWorldMeasureSystemTime);
    if (measure_time) {
        ecs_os_get_time(&time_start);
    }

    ecs_world_t *thread_ctx = world;
    if (stage) {
        thread_ctx = stage->thread_ctx;
    } else {
        stage = &world->stages[0];
    }

    /* Prepare the query iterator */
    ecs_iter_t pit, wit, qit = ecs_query_iter(thread_ctx, system_data->query);
    ecs_iter_t *it = &qit;

    qit.system = system;
    qit.delta_time = delta_time;
    qit.delta_system_time = time_elapsed;
    qit.frame_offset = offset;
    qit.param = param;
    qit.ctx = system_data->ctx;
    qit.binding_ctx = system_data->binding_ctx;

    flecs_defer_begin(world, stage);

    if (offset || limit) {
        pit = ecs_page_iter(it, offset, limit);
        it = &pit;
    }

    if (stage_count > 1 && system_data->multi_threaded) {
        wit = ecs_worker_iter(it, stage_index, stage_count);
        it = &wit;
    }

    ecs_iter_action_t action = system_data->action;
    it->callback = action;
    
    ecs_run_action_t run = system_data->run;
    if (run) {
        run(it);
    } else if (system_data->query->filter.term_count) {
        if (it == &qit) {
            while (ecs_query_next(&qit)) {
                action(&qit);
            }
        } else {
            while (ecs_iter_next(it)) {
                action(it);
            }
        }
    } else {
        action(&qit);
        ecs_iter_fini(&qit);
    }

    if (measure_time) {
        system_data->time_spent += (ecs_ftime_t)ecs_time_measure(&time_start);
    }

    system_data->invoke_count ++;

    flecs_defer_end(world, stage);

    return it->interrupted_by;
}

/* -- Public API -- */

ecs_entity_t ecs_run_w_filter(
    ecs_world_t *world,
    ecs_entity_t system,
    ecs_ftime_t delta_time,
    int32_t offset,
    int32_t limit,
    void *param)
{
    ecs_stage_t *stage = flecs_stage_from_world(&world);
    ecs_system_t *system_data = ecs_poly_get(world, system, ecs_system_t);
    ecs_assert(system_data != NULL, ECS_INVALID_PARAMETER, NULL);
    return ecs_run_intern(world, stage, system, system_data, 0, 0, delta_time, 
        offset, limit, param);
}

ecs_entity_t ecs_run_worker(
    ecs_world_t *world,
    ecs_entity_t system,
    int32_t stage_index,
    int32_t stage_count,
    ecs_ftime_t delta_time,
    void *param)
{
    ecs_stage_t *stage = flecs_stage_from_world(&world);
    ecs_system_t *system_data = ecs_poly_get(world, system, ecs_system_t);
    ecs_assert(system_data != NULL, ECS_INVALID_PARAMETER, NULL);

    return ecs_run_intern(
        world, stage, system, system_data, stage_index, stage_count, 
        delta_time, 0, 0, param);
}

ecs_entity_t ecs_run(
    ecs_world_t *world,
    ecs_entity_t system,
    ecs_ftime_t delta_time,
    void *param)
{
    return ecs_run_w_filter(world, system, delta_time, 0, 0, param);
}

ecs_query_t* ecs_system_get_query(
    const ecs_world_t *world,
    ecs_entity_t system)
{
    const ecs_system_t *s = ecs_poly_get(world, system, ecs_system_t);
    if (s) {
        return s->query;
    } else {
        return NULL;
    }
}

void* ecs_get_system_ctx(
    const ecs_world_t *world,
    ecs_entity_t system)
{
    const ecs_system_t *s = ecs_poly_get(world, system, ecs_system_t);
    if (s) {
        return s->ctx;
    } else {
        return NULL;
    }   
}

void* ecs_get_system_binding_ctx(
    const ecs_world_t *world,
    ecs_entity_t system)
{
    const ecs_system_t *s = ecs_poly_get(world, system, ecs_system_t);
    if (s) {
        return s->binding_ctx;
    } else {
        return NULL;
    }   
}

/* System deinitialization */
static
void flecs_system_fini(ecs_system_t *sys) {
    if (sys->ctx_free) {
        sys->ctx_free(sys->ctx);
    }

    if (sys->binding_ctx_free) {
        sys->binding_ctx_free(sys->binding_ctx);
    }

    ecs_poly_free(sys, ecs_system_t);
}

ecs_entity_t ecs_system_init(
    ecs_world_t *world,
    const ecs_system_desc_t *desc)
{
    ecs_poly_assert(world, ecs_world_t);
    ecs_check(desc != NULL, ECS_INVALID_PARAMETER, NULL);
    ecs_check(desc->_canary == 0, ECS_INVALID_PARAMETER, NULL);
    ecs_assert(!(world->flags & EcsWorldReadonly), 
        ECS_INVALID_WHILE_READONLY, NULL);

    ecs_entity_t entity = desc->entity;
    if (!entity) {
        entity = ecs_new(world, 0);
    }
    EcsPoly *poly = ecs_poly_bind(world, entity, ecs_system_t);
    if (!poly->poly) {
        ecs_system_t *system = ecs_poly_new(ecs_system_t);
        ecs_assert(system != NULL, ECS_INTERNAL_ERROR, NULL);
        
        poly->poly = system;
        system->world = world;
        system->dtor = (ecs_poly_dtor_t)flecs_system_fini;
        system->entity = entity;

        ecs_query_desc_t query_desc = desc->query;
        query_desc.filter.entity = entity;

        ecs_query_t *query = ecs_query_init(world, &query_desc);
        if (!query) {
            ecs_delete(world, entity);
            return 0;
        }

        /* Prevent the system from moving while we're initializing */
        flecs_defer_begin(world, &world->stages[0]);

        system->query = query;
        system->query_entity = query->filter.entity;

        system->run = desc->run;
        system->action = desc->callback;

        system->ctx = desc->ctx;
        system->binding_ctx = desc->binding_ctx;

        system->ctx_free = desc->ctx_free;
        system->binding_ctx_free = desc->binding_ctx_free;

        system->tick_source = desc->tick_source;

        system->multi_threaded = desc->multi_threaded;
        system->no_readonly = desc->no_readonly;

        if (desc->interval != 0 || desc->rate != 0 || desc->tick_source != 0) {
#ifdef FLECS_TIMER
            if (desc->interval != 0) {
                ecs_set_interval(world, entity, desc->interval);
            }

            if (desc->rate) {
                ecs_set_rate(world, entity, desc->rate, desc->tick_source);
            } else if (desc->tick_source) {
                ecs_set_tick_source(world, entity, desc->tick_source);
            }
#else
            ecs_abort(ECS_UNSUPPORTED, "timer module not available");
#endif
        }

        if (ecs_get_name(world, entity)) {
            ecs_trace("#[green]system#[reset] %s created", 
                ecs_get_name(world, entity));
        }

        ecs_defer_end(world);            
    } else {
        ecs_system_t *system = ecs_poly(poly->poly, ecs_system_t);

        if (desc->run) {
            system->run = desc->run;
        }
        if (desc->callback) {
            system->action = desc->callback;
        }

        if (system->ctx_free) {
            if (system->ctx && system->ctx != desc->ctx) {
                system->ctx_free(system->ctx);
            }
        }
        if (system->binding_ctx_free) {
            if (system->binding_ctx && system->binding_ctx != desc->binding_ctx) {
                system->binding_ctx_free(system->binding_ctx);
            }
        }

        if (desc->ctx) {
            system->ctx = desc->ctx;
        }
        if (desc->binding_ctx) {
            system->binding_ctx = desc->binding_ctx;
        }
        if (desc->ctx_free) {
            system->ctx_free = desc->ctx_free;
        }
        if (desc->binding_ctx_free) {
            system->binding_ctx_free = desc->binding_ctx_free;
        }
        if (desc->query.filter.instanced) {
            ECS_BIT_SET(system->query->filter.flags, EcsFilterIsInstanced);
        }
        if (desc->multi_threaded) {
            system->multi_threaded = desc->multi_threaded;
        }
        if (desc->no_readonly) {
            system->no_readonly = desc->no_readonly;
        }

        if (desc->interval != 0 || desc->rate != 0 || desc->tick_source != 0) {
#ifdef FLECS_TIMER
            if (desc->interval != 0) {
                ecs_set_interval(world, entity, desc->interval);
            }
            if (desc->rate != 0) {
                ecs_set_rate(world, entity, desc->rate, desc->tick_source);
            } else if (desc->tick_source) {
                ecs_set_tick_source(world, entity, desc->tick_source);
            }
#else
            ecs_abort(ECS_UNSUPPORTED, "timer module not available");
#endif
        }
    }

    ecs_poly_modified(world, entity, ecs_system_t);

    return entity;
error:
    return 0;
}

void FlecsSystemImport(
    ecs_world_t *world)
{
    ECS_MODULE(world, FlecsSystem);

    ecs_set_name_prefix(world, "Ecs");

    flecs_bootstrap_tag(world, EcsSystem);
    flecs_bootstrap_component(world, EcsTickSource);

    /* Make sure to never inherit system component. This makes sure that any
     * term created for the System component will default to 'self' traversal,
     * which improves efficiency of the query. */
    ecs_add_id(world, EcsSystem, EcsDontInherit);
}

#endif

/**
 * @file json/json.c
 * @brief JSON serializer utilities.
 */

/**
 * @file json/json.h
 * @brief Internal functions for JSON addon.
 */


#ifdef FLECS_JSON

/* Deserialize from JSON */
typedef enum ecs_json_token_t {
    JsonObjectOpen,
    JsonObjectClose,
    JsonArrayOpen,
    JsonArrayClose,
    JsonColon,
    JsonComma,
    JsonNumber,
    JsonString,
    JsonTrue,
    JsonFalse,
    JsonNull,
    JsonLargeString,
    JsonInvalid
} ecs_json_token_t;

const char* flecs_json_parse(
    const char *json,
    ecs_json_token_t *token_kind,
    char *token);

const char* flecs_json_parse_large_string(
    const char *json,
    ecs_strbuf_t *buf);

const char* flecs_json_expect(
    const char *json,
    ecs_json_token_t token_kind,
    char *token,
    const ecs_from_json_desc_t *desc);

const char* flecs_json_expect_member(
    const char *json,
    char *token,
    const ecs_from_json_desc_t *desc);

const char* flecs_json_expect_member_name(
    const char *json,
    char *token,
    const char *member_name,
    const ecs_from_json_desc_t *desc);

const char* flecs_json_skip_object(
    const char *json,
    char *token,
    const ecs_from_json_desc_t *desc);

const char* flecs_json_skip_array(
    const char *json,
    char *token,
    const ecs_from_json_desc_t *desc);

/* Serialize to JSON */
void flecs_json_next(
    ecs_strbuf_t *buf);

void flecs_json_number(
    ecs_strbuf_t *buf,
    double value);

void flecs_json_true(
    ecs_strbuf_t *buf);

void flecs_json_false(
    ecs_strbuf_t *buf);

void flecs_json_bool(
    ecs_strbuf_t *buf,
    bool value);

void flecs_json_array_push(
    ecs_strbuf_t *buf);

void flecs_json_array_pop(
    ecs_strbuf_t *buf);

void flecs_json_object_push(
    ecs_strbuf_t *buf);

void flecs_json_object_pop(
    ecs_strbuf_t *buf);

void flecs_json_string(
    ecs_strbuf_t *buf,
    const char *value);

void flecs_json_string_escape(
    ecs_strbuf_t *buf,
    const char *value);

void flecs_json_member(
    ecs_strbuf_t *buf,
    const char *name);

void flecs_json_membern(
    ecs_strbuf_t *buf,
    const char *name,
    int32_t name_len);

#define flecs_json_memberl(buf, name)\
    flecs_json_membern(buf, name, sizeof(name) - 1)

void flecs_json_path(
    ecs_strbuf_t *buf,
    const ecs_world_t *world,
    ecs_entity_t e);

void flecs_json_label(
    ecs_strbuf_t *buf,
    const ecs_world_t *world,
    ecs_entity_t e);

void flecs_json_color(
    ecs_strbuf_t *buf,
    const ecs_world_t *world,
    ecs_entity_t e);

void flecs_json_id(
    ecs_strbuf_t *buf,
    const ecs_world_t *world,
    ecs_id_t id);

ecs_primitive_kind_t flecs_json_op_to_primitive_kind(
    ecs_meta_type_op_kind_t kind);

#endif

#include <ctype.h>

#ifdef FLECS_JSON

static
const char* flecs_json_token_str(
    ecs_json_token_t token_kind)
{
    switch(token_kind) {
    case JsonObjectOpen: return "{";
    case JsonObjectClose: return "}";
    case JsonArrayOpen: return "[";
    case JsonArrayClose: return "]";
    case JsonColon: return ":";
    case JsonComma: return ",";
    case JsonNumber: return "number";
    case JsonString: return "string";
    case JsonTrue: return "true";
    case JsonFalse: return "false";
    case JsonNull: return "null";
    case JsonInvalid: return "invalid";
    default:
        ecs_abort(ECS_INTERNAL_ERROR, NULL);
    }
    return "invalid";
}

const char* flecs_json_parse(
    const char *json,
    ecs_json_token_t *token_kind,
    char *token)
{
    json = ecs_parse_ws_eol(json);

    char ch = json[0];

    if (ch == '{') {
        token_kind[0] = JsonObjectOpen;
        return json + 1;
    } else if (ch == '}') {
        token_kind[0] = JsonObjectClose;
        return json + 1;
    } else if (ch == '[') {
        token_kind[0] = JsonArrayOpen;
        return json + 1;
    } else if (ch == ']') {
        token_kind[0] = JsonArrayClose;
        return json + 1;
    } else if (ch == ':') {
        token_kind[0] = JsonColon;
        return json + 1;
    } else if (ch == ',') {
        token_kind[0] = JsonComma;
        return json + 1;
    } else if (ch == '"') {
        const char *start = json;
        char *token_ptr = token;
        json ++;
        for (; (ch = json[0]); ) {
            if (ch == '"') {
                json ++;
                token_ptr[0] = '\0';
                break;
            }

            if (token_ptr - token >= ECS_MAX_TOKEN_SIZE) {
                /* Token doesn't fit in buffer, signal to app to try again with
                 * dynamic buffer. */
                token_kind[0] = JsonLargeString;
                return start;
            }

            json = ecs_chrparse(json, token_ptr ++);
        }

        if (!ch) {
            token_kind[0] = JsonInvalid;
            return NULL;
        } else {
            token_kind[0] = JsonString;
            return json;
        }
    } else if (isdigit(ch) || (ch == '-')) {
        token_kind[0] = JsonNumber;
        return ecs_parse_digit(json, token);
    } else if (isalpha(ch)) {
        if (!ecs_os_strncmp(json, "null", 4)) {
            token_kind[0] = JsonNull;
            json += 4;
        } else
        if (!ecs_os_strncmp(json, "true", 4)) {
            token_kind[0] = JsonTrue;
            json += 4;
        } else
        if (!ecs_os_strncmp(json, "false", 5)) {
            token_kind[0] = JsonFalse;
            json += 5;
        }

        if (isalpha(json[0]) || isdigit(json[0])) {
            token_kind[0] = JsonInvalid;
            return NULL;
        }

        return json;
    } else {
        token_kind[0] = JsonInvalid;
        return NULL;
    }
}

const char* flecs_json_parse_large_string(
    const char *json,
    ecs_strbuf_t *buf)
{
    if (json[0] != '"') {
        return NULL; /* can only parse strings */
    }

    char ch, ch_out;
    json ++;
    for (; (ch = json[0]); ) {
        if (ch == '"') {
            json ++;
            break;
        }

        json = ecs_chrparse(json, &ch_out);
        ecs_strbuf_appendch(buf, ch_out);
    }

    if (!ch) {
        return NULL;
    } else {
        return json;
    }
}

const char* flecs_json_expect(
    const char *json,
    ecs_json_token_t token_kind,
    char *token,
    const ecs_from_json_desc_t *desc)
{
    ecs_json_token_t kind = 0;
    json = flecs_json_parse(json, &kind, token);
    if (kind == JsonInvalid) {
        ecs_parser_error(desc->name, desc->expr, json - desc->expr, "invalid json");
        return NULL;
    } else if (kind != token_kind) {
        ecs_parser_error(desc->name, desc->expr, json - desc->expr, "expected %s",
            flecs_json_token_str(token_kind));
        return NULL;
    }
    return json;
}

const char* flecs_json_expect_member(
    const char *json,
    char *token,
    const ecs_from_json_desc_t *desc)
{
    json = flecs_json_expect(json, JsonString, token, desc);
    if (!json) {
        return NULL;
    }
    json = flecs_json_expect(json, JsonColon, token, desc);
    if (!json) {
        return NULL;
    }
    return json;
}

const char* flecs_json_expect_member_name(
    const char *json,
    char *token,
    const char *member_name,
    const ecs_from_json_desc_t *desc)
{
    json = flecs_json_expect_member(json, token, desc);
    if (!json) {
        return NULL;
    }
    if (ecs_os_strcmp(token, member_name)) {
        ecs_parser_error(desc->name, desc->expr, json - desc->expr, 
            "expected member '%s'", member_name);
        return NULL;
    }
    return json;
}

const char* flecs_json_skip_object(
    const char *json,
    char *token,
    const ecs_from_json_desc_t *desc)
{
    ecs_json_token_t token_kind = 0;

    while ((json = flecs_json_parse(json, &token_kind, token))) {
        if (token_kind == JsonObjectOpen) {
            json = flecs_json_skip_object(json, token, desc);
        } else if (token_kind == JsonArrayOpen) {
            json = flecs_json_skip_array(json, token, desc);
        } else if (token_kind == JsonObjectClose) {
            return json;
        } else if (token_kind == JsonArrayClose) {
            ecs_parser_error(desc->name, desc->expr, json - desc->expr, 
                "expected }");
            return NULL;
        }
    }

    ecs_parser_error(desc->name, desc->expr, json - desc->expr, "expected }");
    return NULL;
}

const char* flecs_json_skip_array(
    const char *json,
    char *token,
    const ecs_from_json_desc_t *desc)
{
    ecs_json_token_t token_kind = 0;

    while ((json = flecs_json_parse(json, &token_kind, token))) {
        if (token_kind == JsonObjectOpen) {
            json = flecs_json_skip_object(json, token, desc);
        } else if (token_kind == JsonArrayOpen) {
            json = flecs_json_skip_array(json, token, desc);
        } else if (token_kind == JsonObjectClose) {
            ecs_parser_error(desc->name, desc->expr, json - desc->expr, 
                "expected ]");
            return NULL;
        } else if (token_kind == JsonArrayClose) {
            return json;
        }
    }

    ecs_parser_error(desc->name, desc->expr, json - desc->expr, "expected ]");
    return NULL;
}

void flecs_json_next(
    ecs_strbuf_t *buf)
{
    ecs_strbuf_list_next(buf);
}

void flecs_json_number(
    ecs_strbuf_t *buf,
    double value)
{
    ecs_strbuf_appendflt(buf, value, '"');
}

void flecs_json_true(
    ecs_strbuf_t *buf)
{
    ecs_strbuf_appendlit(buf, "true");
}

void flecs_json_false(
    ecs_strbuf_t *buf)
{
    ecs_strbuf_appendlit(buf, "false");
}

void flecs_json_bool(
    ecs_strbuf_t *buf,
    bool value)
{
    if (value) {
        flecs_json_true(buf);
    } else {
        flecs_json_false(buf);
    }
}

void flecs_json_array_push(
    ecs_strbuf_t *buf)
{
    ecs_strbuf_list_push(buf, "[", ", ");
}

void flecs_json_array_pop(
    ecs_strbuf_t *buf)
{
    ecs_strbuf_list_pop(buf, "]");
}

void flecs_json_object_push(
    ecs_strbuf_t *buf)
{
    ecs_strbuf_list_push(buf, "{", ", ");
}

void flecs_json_object_pop(
    ecs_strbuf_t *buf)
{
    ecs_strbuf_list_pop(buf, "}");
}

void flecs_json_string(
    ecs_strbuf_t *buf,
    const char *value)
{
    ecs_strbuf_appendch(buf, '"');
    ecs_strbuf_appendstr(buf, value);
    ecs_strbuf_appendch(buf, '"');
}

void flecs_json_string_escape(
    ecs_strbuf_t *buf,
    const char *value)
{
    ecs_size_t length = ecs_stresc(NULL, 0, '"', value);
    if (length == ecs_os_strlen(value)) {
        ecs_strbuf_appendch(buf, '"');
        ecs_strbuf_appendstrn(buf, value, length);
        ecs_strbuf_appendch(buf, '"');
    } else {
        char *out = ecs_os_malloc(length + 3);
        ecs_stresc(out + 1, length, '"', value);
        out[0] = '"';
        out[length + 1] = '"';
        out[length + 2] = '\0';
        ecs_strbuf_appendstr_zerocpy(buf, out);
    }
}

void flecs_json_member(
    ecs_strbuf_t *buf,
    const char *name)
{
    flecs_json_membern(buf, name, ecs_os_strlen(name));
}

void flecs_json_membern(
    ecs_strbuf_t *buf,
    const char *name,
    int32_t name_len)
{
    ecs_strbuf_list_appendch(buf, '"');
    ecs_strbuf_appendstrn(buf, name, name_len);
    ecs_strbuf_appendlit(buf, "\":");
}

void flecs_json_path(
    ecs_strbuf_t *buf,
    const ecs_world_t *world,
    ecs_entity_t e)
{
    ecs_strbuf_appendch(buf, '"');
    ecs_get_path_w_sep_buf(world, 0, e, ".", "", buf);
    ecs_strbuf_appendch(buf, '"');
}

void flecs_json_label(
    ecs_strbuf_t *buf,
    const ecs_world_t *world,
    ecs_entity_t e)
{
    const char *lbl = NULL;
#ifdef FLECS_DOC
    lbl = ecs_doc_get_name(world, e);
#else
    lbl = ecs_get_name(world, e);
#endif

    if (lbl) {
        ecs_strbuf_appendch(buf, '"');
        ecs_strbuf_appendstr(buf, lbl);
        ecs_strbuf_appendch(buf, '"');
    } else {
        ecs_strbuf_appendch(buf, '0');
    }
}

void flecs_json_color(
    ecs_strbuf_t *buf,
    const ecs_world_t *world,
    ecs_entity_t e)
{
    (void)world;
    (void)e;

    const char *color = NULL;
#ifdef FLECS_DOC
    color = ecs_doc_get_color(world, e);
#endif

    if (color) {
        ecs_strbuf_appendch(buf, '"');
        ecs_strbuf_appendstr(buf, color);
        ecs_strbuf_appendch(buf, '"');
    } else {
        ecs_strbuf_appendch(buf, '0');
    }
}

void flecs_json_id(
    ecs_strbuf_t *buf,
    const ecs_world_t *world,
    ecs_id_t id)
{
    ecs_strbuf_appendch(buf, '[');

    if (ECS_IS_PAIR(id)) {
        ecs_entity_t first = ecs_pair_first(world, id);
        ecs_entity_t second = ecs_pair_second(world, id);
        ecs_strbuf_appendch(buf, '"');
        ecs_get_path_w_sep_buf(world, 0, first, ".", "", buf);
        ecs_strbuf_appendch(buf, '"');
        ecs_strbuf_appendch(buf, ',');
        ecs_strbuf_appendch(buf, '"');
        ecs_get_path_w_sep_buf(world, 0, second, ".", "", buf);
        ecs_strbuf_appendch(buf, '"');
    } else {
        ecs_strbuf_appendch(buf, '"');
        ecs_get_path_w_sep_buf(world, 0, id & ECS_COMPONENT_MASK, ".", "", buf);
        ecs_strbuf_appendch(buf, '"');
    }

    ecs_strbuf_appendch(buf, ']');
}

ecs_primitive_kind_t flecs_json_op_to_primitive_kind(
    ecs_meta_type_op_kind_t kind) 
{
    return kind - EcsOpPrimitive;
}

#endif

/**
 * @file json/serialize.c
 * @brief Serialize (component) values to JSON strings.
 */


#ifdef FLECS_JSON

/* Cached id records during serialization */
typedef struct ecs_json_ser_idr_t {
    ecs_id_record_t *idr_doc_name;
    ecs_id_record_t *idr_doc_color;
} ecs_json_ser_idr_t;

static
int json_ser_type(
    const ecs_world_t *world,
    const ecs_vec_t *ser, 
    const void *base, 
    ecs_strbuf_t *str);

static
int json_ser_type_ops(
    const ecs_world_t *world,
    ecs_meta_type_op_t *ops,
    int32_t op_count,
    const void *base, 
    ecs_strbuf_t *str,
    int32_t in_array);

static
int json_ser_type_op(
    const ecs_world_t *world,
    ecs_meta_type_op_t *op, 
    const void *base,
    ecs_strbuf_t *str);

/* Serialize enumeration */
static
int json_ser_enum(
    const ecs_world_t *world,
    ecs_meta_type_op_t *op, 
    const void *base, 
    ecs_strbuf_t *str) 
{
    const EcsEnum *enum_type = ecs_get(world, op->type, EcsEnum);
    ecs_check(enum_type != NULL, ECS_INVALID_PARAMETER, NULL);

    int32_t value = *(int32_t*)base;
    
    /* Enumeration constants are stored in a map that is keyed on the
     * enumeration value. */
    ecs_enum_constant_t *constant = ecs_map_get_deref(&enum_type->constants,
        ecs_enum_constant_t, (ecs_map_key_t)value);
    if (!constant) {
        /* If the value is not found, it is not a valid enumeration constant */
        char *name = ecs_get_fullpath(world, op->type);
        ecs_err("enumeration value '%d' of type '%s' is not a valid constant", 
            value, name);
        ecs_os_free(name);
        goto error;
    }

    ecs_strbuf_appendch(str, '"');
    ecs_strbuf_appendstr(str, ecs_get_name(world, constant->constant));
    ecs_strbuf_appendch(str, '"');

    return 0;
error:
    return -1;
}

/* Serialize bitmask */
static
int json_ser_bitmask(
    const ecs_world_t *world,
    ecs_meta_type_op_t *op, 
    const void *ptr, 
    ecs_strbuf_t *str) 
{
    const EcsBitmask *bitmask_type = ecs_get(world, op->type, EcsBitmask);
    ecs_check(bitmask_type != NULL, ECS_INVALID_PARAMETER, NULL);

    uint32_t value = *(uint32_t*)ptr;
    if (!value) {
        ecs_strbuf_appendch(str, '0');
        return 0;
    }

    ecs_strbuf_list_push(str, "\"", "|");

    /* Multiple flags can be set at a given time. Iterate through all the flags
     * and append the ones that are set. */
    ecs_map_iter_t it = ecs_map_iter(&bitmask_type->constants);
    while (ecs_map_next(&it)) {
        ecs_bitmask_constant_t *constant = ecs_map_ptr(&it);
        ecs_map_key_t key = ecs_map_key(&it);
        if ((value & key) == key) {
            ecs_strbuf_list_appendstr(str, 
                ecs_get_name(world, constant->constant));
            value -= (uint32_t)key;
        }
    }

    if (value != 0) {
        /* All bits must have been matched by a constant */
        char *name = ecs_get_fullpath(world, op->type);
        ecs_err("bitmask value '%u' of type '%s' contains invalid/unknown bits", 
            value, name);
        ecs_os_free(name);
        goto error;
    }

    ecs_strbuf_list_pop(str, "\"");

    return 0;
error:
    return -1;
}

/* Serialize elements of a contiguous array */
static
int json_ser_elements(
    const ecs_world_t *world,
    ecs_meta_type_op_t *ops, 
    int32_t op_count,
    const void *base, 
    int32_t elem_count, 
    int32_t elem_size,
    ecs_strbuf_t *str,
    bool is_array)
{
    flecs_json_array_push(str);

    const void *ptr = base;

    int i;
    for (i = 0; i < elem_count; i ++) {
        ecs_strbuf_list_next(str);
        if (json_ser_type_ops(world, ops, op_count, ptr, str, is_array)) {
            return -1;
        }
        ptr = ECS_OFFSET(ptr, elem_size);
    }

    flecs_json_array_pop(str);

    return 0;
}

static
int json_ser_type_elements(
    const ecs_world_t *world,
    ecs_entity_t type, 
    const void *base, 
    int32_t elem_count, 
    ecs_strbuf_t *str,
    bool is_array)
{
    const EcsMetaTypeSerialized *ser = ecs_get(
        world, type, EcsMetaTypeSerialized);
    ecs_assert(ser != NULL, ECS_INTERNAL_ERROR, NULL);

    const EcsComponent *comp = ecs_get(world, type, EcsComponent);
    ecs_assert(comp != NULL, ECS_INTERNAL_ERROR, NULL);

    ecs_meta_type_op_t *ops = ecs_vec_first_t(&ser->ops, ecs_meta_type_op_t);
    int32_t op_count = ecs_vec_count(&ser->ops);

    return json_ser_elements(
        world, ops, op_count, base, elem_count, comp->size, str, is_array);
}

/* Serialize array */
static
int json_ser_array(
    const ecs_world_t *world,
    ecs_meta_type_op_t *op, 
    const void *ptr, 
    ecs_strbuf_t *str) 
{
    const EcsArray *a = ecs_get(world, op->type, EcsArray);
    ecs_assert(a != NULL, ECS_INTERNAL_ERROR, NULL);

    return json_ser_type_elements(
        world, a->type, ptr, a->count, str, true);
}

/* Serialize vector */
static
int json_ser_vector(
    const ecs_world_t *world,
    ecs_meta_type_op_t *op, 
    const void *base, 
    ecs_strbuf_t *str) 
{
    const ecs_vec_t *value = base;
    const EcsVector *v = ecs_get(world, op->type, EcsVector);
    ecs_assert(v != NULL, ECS_INTERNAL_ERROR, NULL);

    int32_t count = ecs_vec_count(value);
    void *array = ecs_vec_first(value);

    /* Serialize contiguous buffer of vector */
    return json_ser_type_elements(world, v->type, array, count, str, false);
}

typedef struct json_serializer_ctx_t {
    ecs_strbuf_t *str;
    bool is_collection;
    bool is_struct;
} json_serializer_ctx_t;

static
int json_ser_custom_value(
    const ecs_serializer_t *ser,
    ecs_entity_t type,
    const void *value)
{
    json_serializer_ctx_t *json_ser = ser->ctx;
    if (json_ser->is_collection) {
        ecs_strbuf_list_next(json_ser->str);
    }
    return ecs_ptr_to_json_buf(ser->world, type, value, json_ser->str);
}

static
int json_ser_custom_member(
    const ecs_serializer_t *ser,
    const char *name)
{
    json_serializer_ctx_t *json_ser = ser->ctx;
    if (!json_ser->is_struct) {
        ecs_err("serializer::member can only be called for structs");
        return -1;
    }
    flecs_json_member(json_ser->str, name);
    return 0;
}

static
int json_ser_custom_type(
    const ecs_world_t *world,
    ecs_meta_type_op_t *op, 
    const void *base, 
    ecs_strbuf_t *str)
{
    const EcsOpaque *ct = ecs_get(world, op->type, EcsOpaque);
    ecs_assert(ct != NULL, ECS_INVALID_OPERATION, NULL);
    ecs_assert(ct->as_type != 0, ECS_INVALID_OPERATION, NULL);
    ecs_assert(ct->serialize != NULL, ECS_INVALID_OPERATION, 
        ecs_get_name(world, op->type));

    const EcsMetaType *pt = ecs_get(world, ct->as_type, EcsMetaType);
    ecs_assert(pt != NULL, ECS_INVALID_OPERATION, NULL);

    ecs_type_kind_t kind = pt->kind;
    bool is_collection = false;
    bool is_struct = false;

    if (kind == EcsStructType) {
        flecs_json_object_push(str);
        is_struct = true;
    } else if (kind == EcsArrayType || kind == EcsVectorType) {
        flecs_json_array_push(str);
        is_collection = true;
    }

    json_serializer_ctx_t json_ser = {
        .str = str,
        .is_struct = is_struct,
        .is_collection = is_collection
    };

    ecs_serializer_t ser = {
        .world = world,
        .value = json_ser_custom_value,
        .member = json_ser_custom_member,
        .ctx = &json_ser
    };

    if (ct->serialize(&ser, base)) {
        return -1;
    }

    if (kind == EcsStructType) {
        flecs_json_object_pop(str);
    } else if (kind == EcsArrayType || kind == EcsVectorType) {
        flecs_json_array_pop(str);
    }

    return 0;
}

/* Forward serialization to the different type kinds */
static
int json_ser_type_op(
    const ecs_world_t *world,
    ecs_meta_type_op_t *op, 
    const void *ptr,
    ecs_strbuf_t *str) 
{
    switch(op->kind) {
    case EcsOpPush:
    case EcsOpPop:
        /* Should not be parsed as single op */
        ecs_throw(ECS_INVALID_PARAMETER, NULL);
        break;
    case EcsOpF32:
        ecs_strbuf_appendflt(str, 
            (ecs_f64_t)*(ecs_f32_t*)ECS_OFFSET(ptr, op->offset), '"');
        break;
    case EcsOpF64:
        ecs_strbuf_appendflt(str, 
            *(ecs_f64_t*)ECS_OFFSET(ptr, op->offset), '"');
        break;
    case EcsOpEnum:
        if (json_ser_enum(world, op, ECS_OFFSET(ptr, op->offset), str)) {
            goto error;
        }
        break;
    case EcsOpBitmask:
        if (json_ser_bitmask(world, op, ECS_OFFSET(ptr, op->offset), str)) {
            goto error;
        }
        break;
    case EcsOpArray:
        if (json_ser_array(world, op, ECS_OFFSET(ptr, op->offset), str)) {
            goto error;
        }
        break;
    case EcsOpVector:
        if (json_ser_vector(world, op, ECS_OFFSET(ptr, op->offset), str)) {
            goto error;
        }
        break;
    case EcsOpOpaque:
        if (json_ser_custom_type(world, op, ECS_OFFSET(ptr, op->offset), str)) {
            goto error;
        }
        break;
    case EcsOpEntity: {
        ecs_entity_t e = *(ecs_entity_t*)ECS_OFFSET(ptr, op->offset);
        if (!e) {
            ecs_strbuf_appendch(str, '0');
        } else {
            flecs_json_path(str, world, e);
        }
        break;
    }

    default:
        if (ecs_primitive_to_expr_buf(world, 
            flecs_json_op_to_primitive_kind(op->kind), 
            ECS_OFFSET(ptr, op->offset), str)) 
        {
            /* Unknown operation */
            ecs_throw(ECS_INTERNAL_ERROR, NULL);
            return -1;
        }
        break;
    }

    return 0;
error:
    return -1;
}

/* Iterate over a slice of the type ops array */
static
int json_ser_type_ops(
    const ecs_world_t *world,
    ecs_meta_type_op_t *ops,
    int32_t op_count,
    const void *base,
    ecs_strbuf_t *str,
    int32_t in_array)
{
    for (int i = 0; i < op_count; i ++) {
        ecs_meta_type_op_t *op = &ops[i];

        if (in_array <= 0) {
            if (op->name) {
                flecs_json_member(str, op->name);
            }

            int32_t elem_count = op->count;
            if (elem_count > 1) {
                /* Serialize inline array */
                if (json_ser_elements(world, op, op->op_count, base,
                    elem_count, op->size, str, true))
                {
                    return -1;
                }

                i += op->op_count - 1;
                continue;
            }
        }
        
        switch(op->kind) {
        case EcsOpPush:
            flecs_json_object_push(str);
            in_array --;
            break;
        case EcsOpPop:
            flecs_json_object_pop(str);
            in_array ++;
            break;
        default:
            if (json_ser_type_op(world, op, base, str)) {
                goto error;
            }
            break;
        }
    }

    return 0;
error:
    return -1;
}

/* Iterate over the type ops of a type */
static
int json_ser_type(
    const ecs_world_t *world,
    const ecs_vec_t *v_ops,
    const void *base, 
    ecs_strbuf_t *str) 
{
    ecs_meta_type_op_t *ops = ecs_vec_first_t(v_ops, ecs_meta_type_op_t);
    int32_t count = ecs_vec_count(v_ops);
    return json_ser_type_ops(world, ops, count, base, str, 0);
}

static
int array_to_json_buf_w_type_data(
    const ecs_world_t *world,
    const void *ptr,
    int32_t count,
    ecs_strbuf_t *buf,
    const EcsComponent *comp,
    const EcsMetaTypeSerialized *ser)
{
    if (count) {
        ecs_size_t size = comp->size;

        flecs_json_array_push(buf);

        do {
            ecs_strbuf_list_next(buf);
            if (json_ser_type(world, &ser->ops, ptr, buf)) {
                return -1;
            }

            ptr = ECS_OFFSET(ptr, size);
        } while (-- count);

        flecs_json_array_pop(buf);
    } else {
        if (json_ser_type(world, &ser->ops, ptr, buf)) {
            return -1;
        }
    }

    return 0;
}

int ecs_array_to_json_buf(
    const ecs_world_t *world,
    ecs_entity_t type,
    const void *ptr,
    int32_t count,
    ecs_strbuf_t *buf)
{
    const EcsComponent *comp = ecs_get(world, type, EcsComponent);
    if (!comp) {
        char *path = ecs_get_fullpath(world, type);
        ecs_err("cannot serialize to JSON, '%s' is not a component", path);
        ecs_os_free(path);
        return -1;
    }

    const EcsMetaTypeSerialized *ser = ecs_get(
        world, type, EcsMetaTypeSerialized);
    if (!ser) {
        char *path = ecs_get_fullpath(world, type);
        ecs_err("cannot serialize to JSON, '%s' has no reflection data", path);
        ecs_os_free(path);
        return -1;
    }

    return array_to_json_buf_w_type_data(world, ptr, count, buf, comp, ser);
}

char* ecs_array_to_json(
    const ecs_world_t *world, 
    ecs_entity_t type, 
    const void* ptr,
    int32_t count)
{
    ecs_strbuf_t str = ECS_STRBUF_INIT;

    if (ecs_array_to_json_buf(world, type, ptr, count, &str) != 0) {
        ecs_strbuf_reset(&str);
        return NULL;
    }

    return ecs_strbuf_get(&str);
}

int ecs_ptr_to_json_buf(
    const ecs_world_t *world,
    ecs_entity_t type,
    const void *ptr,
    ecs_strbuf_t *buf)
{
    return ecs_array_to_json_buf(world, type, ptr, 0, buf);
}

char* ecs_ptr_to_json(
    const ecs_world_t *world, 
    ecs_entity_t type, 
    const void* ptr)
{
    return ecs_array_to_json(world, type, ptr, 0);
}

static
bool flecs_json_skip_id(
    const ecs_world_t *world,
    ecs_id_t id,
    const ecs_entity_to_json_desc_t *desc,
    ecs_entity_t ent,
    ecs_entity_t inst,
    ecs_entity_t *pred_out,
    ecs_entity_t *obj_out,
    ecs_entity_t *role_out,
    bool *hidden_out)
{
    bool is_base = ent != inst;
    ecs_entity_t pred = 0, obj = 0, role = 0;
    bool hidden = false;

    if (ECS_HAS_ID_FLAG(id, PAIR)) {
        pred = ecs_pair_first(world, id);
        obj = ecs_pair_second(world, id);
    } else {
        pred = id & ECS_COMPONENT_MASK;
        if (id & ECS_ID_FLAGS_MASK) {
            role = id & ECS_ID_FLAGS_MASK;
        }
    }

    if (!desc || !desc->serialize_meta_ids) {
        if (pred == EcsIsA || pred == EcsChildOf ||
            pred == ecs_id(EcsIdentifier)) 
        {
            return true;
        }
#ifdef FLECS_DOC
        if (pred == ecs_id(EcsDocDescription)) {
            return true;
        }
#endif
    }

    if (is_base) {
        if (ecs_has_id(world, pred, EcsDontInherit)) {
            return true;
        }
    }
    if (!desc || !desc->serialize_private) {
        if (ecs_has_id(world, pred, EcsPrivate)) {
            return true;
        }
    }
    if (is_base) {
        if (ecs_get_target_for_id(world, inst, EcsIsA, id) != ent) {
            hidden = true;
        }
    }
    if (hidden && (!desc || !desc->serialize_hidden)) {
        return true;
    }

    *pred_out = pred;
    *obj_out = obj;
    *role_out = role;
    if (hidden_out) *hidden_out = hidden;

    return false;
}

static
int flecs_json_append_type_labels(
    const ecs_world_t *world, 
    ecs_strbuf_t *buf,
    const ecs_id_t *ids,
    int32_t count,
    ecs_entity_t ent, 
    ecs_entity_t inst,
    const ecs_entity_to_json_desc_t *desc) 
{
    (void)world; (void)buf; (void)ids; (void)count; (void)ent; (void)inst; 
    (void)desc;
    
#ifdef FLECS_DOC
    if (!desc || !desc->serialize_id_labels) {
        return 0;
    }

    flecs_json_memberl(buf, "id_labels");
    flecs_json_array_push(buf);

    int32_t i;
    for (i = 0; i < count; i ++) {
        ecs_entity_t pred = 0, obj = 0, role = 0;
        if (flecs_json_skip_id(world, ids[i], desc, ent, inst, &pred, &obj, &role, 0)) {
            continue;
        }

        if (obj && (pred == EcsUnion)) {
            pred = obj;
            obj = ecs_get_target(world, ent, pred, 0);
            if (!ecs_is_alive(world, obj)) {
                /* Union relationships aren't automatically cleaned up, so they
                 * can contain invalid entity ids. Don't serialize value until
                 * relationship is valid again. */
                continue;
            }
        }

        if (desc && desc->serialize_id_labels) {
            flecs_json_next(buf);

            flecs_json_array_push(buf);
            flecs_json_next(buf);
            flecs_json_label(buf, world, pred);
            if (obj) {
                flecs_json_next(buf);
                flecs_json_label(buf, world, obj);
            }

            flecs_json_array_pop(buf);
        }
    }

    flecs_json_array_pop(buf);
#endif
    return 0;
}

static
int flecs_json_append_type_values(
    const ecs_world_t *world, 
    ecs_strbuf_t *buf, 
    const ecs_id_t *ids,
    int32_t count,
    ecs_entity_t ent, 
    ecs_entity_t inst,
    const ecs_entity_to_json_desc_t *desc) 
{
    if (!desc || !desc->serialize_values) {
        return 0;
    }

    flecs_json_memberl(buf, "values");
    flecs_json_array_push(buf);

    int32_t i;
    for (i = 0; i < count; i ++) {
        bool hidden;
        ecs_entity_t pred = 0, obj = 0, role = 0;
        ecs_id_t id = ids[i];
        if (flecs_json_skip_id(world, id, desc, ent, inst, &pred, &obj, &role, 
            &hidden)) 
        {
            continue;
        }

        if (!hidden) {
            bool serialized = false;
            ecs_entity_t typeid = ecs_get_typeid(world, id);
            if (typeid) {
                const EcsMetaTypeSerialized *ser = ecs_get(
                    world, typeid, EcsMetaTypeSerialized);
                if (ser) {
                    const void *ptr = ecs_get_id(world, ent, id);
                    ecs_assert(ptr != NULL, ECS_INTERNAL_ERROR, NULL);

                    flecs_json_next(buf);
                    if (json_ser_type(world, &ser->ops, ptr, buf) != 0) {
                        /* Entity contains invalid value */
                        return -1;
                    }
                    serialized = true;
                }
            }
            if (!serialized) {
                flecs_json_next(buf);
                flecs_json_number(buf, 0);
            }
        } else {
            if (!desc || desc->serialize_hidden) {
                flecs_json_next(buf);
                flecs_json_number(buf, 0);
            }
        }
    }

    flecs_json_array_pop(buf);
    
    return 0;
}

static
int flecs_json_append_type_info(
    const ecs_world_t *world, 
    ecs_strbuf_t *buf, 
    const ecs_id_t *ids,
    int32_t count,
    ecs_entity_t ent, 
    ecs_entity_t inst,
    const ecs_entity_to_json_desc_t *desc) 
{
    if (!desc || !desc->serialize_type_info) {
        return 0;
    }

    flecs_json_memberl(buf, "type_info");
    flecs_json_array_push(buf);

    int32_t i;
    for (i = 0; i < count; i ++) {
        bool hidden;
        ecs_entity_t pred = 0, obj = 0, role = 0;
        ecs_id_t id = ids[i];
        if (flecs_json_skip_id(world, id, desc, ent, inst, &pred, &obj, &role, 
            &hidden)) 
        {
            continue;
        }

        if (!hidden) {
            ecs_entity_t typeid = ecs_get_typeid(world, id);
            if (typeid) {
                flecs_json_next(buf);
                if (ecs_type_info_to_json_buf(world, typeid, buf) != 0) {
                    return -1;
                }
            } else {
                flecs_json_next(buf);
                flecs_json_number(buf, 0);
            }
        } else {
            if (!desc || desc->serialize_hidden) {
                flecs_json_next(buf);
                flecs_json_number(buf, 0);
            }
        }
    }

    flecs_json_array_pop(buf);
    
    return 0;
}

static
int flecs_json_append_type_hidden(
    const ecs_world_t *world, 
    ecs_strbuf_t *buf, 
    const ecs_id_t *ids,
    int32_t count,
    ecs_entity_t ent, 
    ecs_entity_t inst,
    const ecs_entity_to_json_desc_t *desc) 
{
    if (!desc || !desc->serialize_hidden) {
        return 0;
    }

    if (ent == inst) {
        return 0; /* if this is not a base, components are never hidden */
    }

    flecs_json_memberl(buf, "hidden");
    flecs_json_array_push(buf);

    int32_t i;
    for (i = 0; i < count; i ++) {
        bool hidden;
        ecs_entity_t pred = 0, obj = 0, role = 0;
        ecs_id_t id = ids[i];
        if (flecs_json_skip_id(world, id, desc, ent, inst, &pred, &obj, &role, 
            &hidden)) 
        {
            continue;
        }

        flecs_json_next(buf);
        flecs_json_bool(buf, hidden);
    }

    flecs_json_array_pop(buf);
    
    return 0;
}

static
int flecs_json_append_type(
    const ecs_world_t *world, 
    ecs_strbuf_t *buf, 
    ecs_entity_t ent, 
    ecs_entity_t inst,
    const ecs_entity_to_json_desc_t *desc) 
{
    const ecs_id_t *ids = NULL;
    int32_t i, count = 0;

    const ecs_type_t *type = ecs_get_type(world, ent);
    if (type) {
        ids = type->array;
        count = type->count;
    }

    flecs_json_memberl(buf, "ids");
    flecs_json_array_push(buf);

    for (i = 0; i < count; i ++) {
        ecs_entity_t pred = 0, obj = 0, role = 0;
        if (flecs_json_skip_id(world, ids[i], desc, ent, inst, &pred, &obj, &role, 0)) {
            continue;
        }

        if (obj && (pred == EcsUnion)) {
            pred = obj;
            obj = ecs_get_target(world, ent, pred, 0);
            if (!ecs_is_alive(world, obj)) {
                /* Union relationships aren't automatically cleaned up, so they
                 * can contain invalid entity ids. Don't serialize value until
                 * relationship is valid again. */
                continue;
            }
        }

        flecs_json_next(buf);
        flecs_json_array_push(buf);
        flecs_json_next(buf);
        flecs_json_path(buf, world, pred);
        if (obj || role) {
            flecs_json_next(buf);
            if (obj) {
                flecs_json_path(buf, world, obj);
            } else {
                flecs_json_number(buf, 0);
            }
            if (role) {
                flecs_json_next(buf);
                flecs_json_string(buf, ecs_id_flag_str(role));
            }
        }
        flecs_json_array_pop(buf);
    }

    flecs_json_array_pop(buf);

    if (flecs_json_append_type_labels(world, buf, ids, count, ent, inst, desc)) {
        return -1;
    }
    
    if (flecs_json_append_type_values(world, buf, ids, count, ent, inst, desc)) {
        return -1;
    }

    if (flecs_json_append_type_info(world, buf, ids, count, ent, inst, desc)) {
        return -1;
    }

    if (flecs_json_append_type_hidden(world, buf, ids, count, ent, inst, desc)) {
        return -1;
    }

    return 0;
}

static
int flecs_json_append_base(
    const ecs_world_t *world, 
    ecs_strbuf_t *buf, 
    ecs_entity_t ent, 
    ecs_entity_t inst,
    const ecs_entity_to_json_desc_t *desc) 
{
    const ecs_type_t *type = ecs_get_type(world, ent);
    ecs_id_t *ids = NULL;
    int32_t i, count = 0;
    if (type) {
        ids = type->array;
        count = type->count;
    }

    for (i = 0; i < count; i ++) {
        ecs_id_t id = ids[i];
        if (ECS_HAS_RELATION(id, EcsIsA)) {
            if (flecs_json_append_base(world, buf, ecs_pair_second(world, id), inst, desc)) 
            {
                return -1;
            }
        }
    }

    ecs_strbuf_list_next(buf);
    flecs_json_object_push(buf);
    flecs_json_memberl(buf, "path");
    flecs_json_path(buf, world, ent);

    if (flecs_json_append_type(world, buf, ent, inst, desc)) {
        return -1;
    }

    flecs_json_object_pop(buf);

    return 0;
}

int ecs_entity_to_json_buf(
    const ecs_world_t *world,
    ecs_entity_t entity,
    ecs_strbuf_t *buf,
    const ecs_entity_to_json_desc_t *desc)
{
    if (!entity || !ecs_is_valid(world, entity)) {
        return -1;
    }

    flecs_json_object_push(buf);

    if (!desc || desc->serialize_path) {
        flecs_json_memberl(buf, "path");
        flecs_json_path(buf, world, entity);
    }

#ifdef FLECS_DOC
    if (desc && desc->serialize_label) {
        flecs_json_memberl(buf, "label");
        const char *doc_name = ecs_doc_get_name(world, entity);
        if (doc_name) {
            flecs_json_string_escape(buf, doc_name);
        } else {
            char num_buf[20];
            ecs_os_sprintf(num_buf, "%u", (uint32_t)entity);
            flecs_json_string(buf, num_buf);
        }
    }

    if (desc && desc->serialize_brief) {
        const char *doc_brief = ecs_doc_get_brief(world, entity);
        if (doc_brief) {
            flecs_json_memberl(buf, "brief");
            flecs_json_string_escape(buf, doc_brief);
        }
    }

    if (desc && desc->serialize_link) {
        const char *doc_link = ecs_doc_get_link(world, entity);
        if (doc_link) {
            flecs_json_memberl(buf, "link");
            flecs_json_string_escape(buf, doc_link);
        }
    }

    if (desc && desc->serialize_color) {
        const char *doc_color = ecs_doc_get_color(world, entity);
        if (doc_color) {
            flecs_json_memberl(buf, "color");
            flecs_json_string_escape(buf, doc_color);
        }
    }
#endif

    const ecs_type_t *type = ecs_get_type(world, entity);
    ecs_id_t *ids = NULL;
    int32_t i, count = 0;
    if (type) {
        ids = type->array;
        count = type->count;
    }

    if (!desc || desc->serialize_base) {
        if (ecs_has_pair(world, entity, EcsIsA, EcsWildcard)) {
            flecs_json_memberl(buf, "is_a");
            flecs_json_array_push(buf);

            for (i = 0; i < count; i ++) {
                ecs_id_t id = ids[i];
                if (ECS_HAS_RELATION(id, EcsIsA)) {
                    if (flecs_json_append_base(
                        world, buf, ecs_pair_second(world, id), entity, desc)) 
                    {
                        return -1;
                    }
                }
            }

            flecs_json_array_pop(buf);
        }
    }

    if (flecs_json_append_type(world, buf, entity, entity, desc)) {
        goto error;
    }

    if (desc && desc->serialize_alerts) {
#ifdef FLECS_ALERTS
        const EcsAlertsActive *alerts = ecs_get(world, entity, EcsAlertsActive);
        if (alerts) {
            flecs_json_memberl(buf, "alerts");
            flecs_json_array_push(buf);
            ecs_map_iter_t it = ecs_map_iter(&alerts->alerts);
            while (ecs_map_next(&it)) {
                flecs_json_next(buf);
                flecs_json_object_push(buf);
                ecs_entity_t ai = ecs_map_value(&it);
                char *alert_name = ecs_get_fullpath(world, ai);
                flecs_json_memberl(buf, "alert");
                flecs_json_string(buf, alert_name);
                ecs_os_free(alert_name);

                const EcsAlertInstance *alert = ecs_get(
                    world, ai, EcsAlertInstance);
                if (alert) {
                    flecs_json_memberl(buf, "message");
                    flecs_json_string(buf, alert->message);
                }
                flecs_json_object_pop(buf);
            }
            flecs_json_array_pop(buf);
        }
#endif
    }

    flecs_json_object_pop(buf);

    return 0;
error:
    return -1;
}

char* ecs_entity_to_json(
    const ecs_world_t *world,
    ecs_entity_t entity,
    const ecs_entity_to_json_desc_t *desc)
{
    ecs_strbuf_t buf = ECS_STRBUF_INIT;

    if (ecs_entity_to_json_buf(world, entity, &buf, desc) != 0) {
        ecs_strbuf_reset(&buf);
        return NULL;
    }

    return ecs_strbuf_get(&buf);
}

static
bool flecs_json_skip_variable(
    const char *name)
{
    if (!name || name[0] == '_' || !ecs_os_strcmp(name, "this")) {
        return true;
    } else {
        return false;
    }
}

static
void flecs_json_serialize_id(
    const ecs_world_t *world,
    ecs_id_t id,
    ecs_strbuf_t *buf) 
{
    flecs_json_id(buf, world, id);
}

static
void flecs_json_serialize_iter_ids(
    const ecs_world_t *world,
    const ecs_iter_t *it, 
    ecs_strbuf_t *buf) 
{
    int32_t field_count = it->field_count;
    if (!field_count) {
        return;
    }

    flecs_json_memberl(buf, "ids");
    flecs_json_array_push(buf);

    for (int i = 0; i < field_count; i ++) {
        flecs_json_next(buf);
        flecs_json_serialize_id(world, it->terms[i].id, buf);
    }

    flecs_json_array_pop(buf);
}

static
void flecs_json_serialize_id_str(
    const ecs_world_t *world,
    ecs_id_t id,
    ecs_strbuf_t *buf)
{
    ecs_strbuf_appendch(buf, '"');
    if (ECS_IS_PAIR(id)) {
        ecs_entity_t first = ecs_pair_first(world, id);
        ecs_entity_t second = ecs_pair_first(world, id);
        ecs_strbuf_appendch(buf, '(');
        ecs_get_path_w_sep_buf(world, 0, first, ".", "", buf);
        ecs_strbuf_appendch(buf, ',');
        ecs_get_path_w_sep_buf(world, 0, second, ".", "", buf);
        ecs_strbuf_appendch(buf, ')');
    } else {
        ecs_get_path_w_sep_buf(
            world, 0, id & ECS_COMPONENT_MASK, ".", "", buf);
    }
    ecs_strbuf_appendch(buf, '"');
}

static
void flecs_json_serialize_type_info(
    const ecs_world_t *world,
    const ecs_iter_t *it, 
    ecs_strbuf_t *buf) 
{
    int32_t field_count = it->field_count;
    if (!field_count) {
        return;
    }

    if (it->flags & EcsIterNoData) {
        return;
    }

    flecs_json_memberl(buf, "type_info");
    flecs_json_object_push(buf);

    for (int i = 0; i < field_count; i ++) {
        flecs_json_next(buf);
        ecs_entity_t typeid = 0;
        if (it->terms[i].inout != EcsInOutNone) {
            typeid = ecs_get_typeid(world, it->terms[i].id);
        }
        if (typeid) {
            flecs_json_serialize_id_str(world, typeid, buf);
            ecs_strbuf_appendch(buf, ':');
            ecs_type_info_to_json_buf(world, typeid, buf);
        } else {
            flecs_json_serialize_id_str(world, it->terms[i].id, buf);
            ecs_strbuf_appendlit(buf, ":0");
        }
    }

    flecs_json_object_pop(buf);
}

static
void flecs_json_serialize_iter_variables(ecs_iter_t *it, ecs_strbuf_t *buf) {
    char **variable_names = it->variable_names;
    int32_t var_count = it->variable_count;
    int32_t actual_count = 0;

    for (int i = 0; i < var_count; i ++) {
        const char *var_name = variable_names[i];
        if (flecs_json_skip_variable(var_name)) continue;

        if (!actual_count) {
            flecs_json_memberl(buf, "vars");
            flecs_json_array_push(buf);
            actual_count ++;
        }

        ecs_strbuf_list_next(buf);
        flecs_json_string(buf, var_name);
    }

    if (actual_count) {
        flecs_json_array_pop(buf);
    }
}

static
void flecs_json_serialize_iter_result_ids(
    const ecs_world_t *world,
    const ecs_iter_t *it,
    ecs_strbuf_t *buf)
{
    flecs_json_memberl(buf, "ids");
    flecs_json_array_push(buf);

    for (int i = 0; i < it->field_count; i ++) {
        flecs_json_next(buf);
        flecs_json_serialize_id(world, ecs_field_id(it, i + 1), buf);
    }

    flecs_json_array_pop(buf);
}

static
void flecs_json_serialize_iter_result_table_type(
    const ecs_world_t *world,
    const ecs_iter_t *it,
    ecs_strbuf_t *buf)
{
    if (!it->table) {
        return;
    }

    flecs_json_memberl(buf, "ids");
    flecs_json_array_push(buf);

    ecs_type_t *type = &it->table->type;
    for (int i = 0; i < type->count; i ++) {
        flecs_json_next(buf);
        flecs_json_serialize_id(world, type->array[i], buf);
    }

    flecs_json_array_pop(buf);
}

static
void flecs_json_serialize_iter_result_sources(
    const ecs_world_t *world,
    const ecs_iter_t *it,
    ecs_strbuf_t *buf)
{
    flecs_json_memberl(buf, "sources");
    flecs_json_array_push(buf);

    for (int i = 0; i < it->field_count; i ++) {
        flecs_json_next(buf);
        ecs_entity_t subj = it->sources[i];
        if (subj) {            
            flecs_json_path(buf, world, subj);
        } else {
            ecs_strbuf_appendch(buf, '0');
        }
    }

    flecs_json_array_pop(buf);
}

static
void flecs_json_serialize_iter_result_is_set(
    const ecs_iter_t *it,
    ecs_strbuf_t *buf)
{
    if (!(it->flags & EcsIterHasCondSet)) {
        return;
    }

    flecs_json_memberl(buf, "is_set");
    flecs_json_array_push(buf);

    for (int i = 0; i < it->field_count; i ++) {
        ecs_strbuf_list_next(buf);
        if (ecs_field_is_set(it, i + 1)) {
            flecs_json_true(buf);
        } else {
            flecs_json_false(buf);
        }
    }

    flecs_json_array_pop(buf);
}

static
void flecs_json_serialize_iter_result_variables(
    const ecs_world_t *world,
    const ecs_iter_t *it,
    ecs_strbuf_t *buf) 
{
    char **variable_names = it->variable_names;
    ecs_var_t *variables = it->variables;
    int32_t var_count = it->variable_count;
    int32_t actual_count = 0;

    for (int i = 0; i < var_count; i ++) {
        const char *var_name = variable_names[i];
        if (flecs_json_skip_variable(var_name)) continue;

        if (!actual_count) {
            flecs_json_memberl(buf, "vars");
            flecs_json_array_push(buf);
            actual_count ++;
        }

        ecs_strbuf_list_next(buf);
        flecs_json_path(buf, world, variables[i].entity);
    }

    if (actual_count) {
        flecs_json_array_pop(buf);
    }
}

static
void flecs_json_serialize_iter_result_variable_labels(
    const ecs_world_t *world,
    const ecs_iter_t *it,
    ecs_strbuf_t *buf) 
{
    char **variable_names = it->variable_names;
    ecs_var_t *variables = it->variables;
    int32_t var_count = it->variable_count;
    int32_t actual_count = 0;

    for (int i = 0; i < var_count; i ++) {
        const char *var_name = variable_names[i];
        if (flecs_json_skip_variable(var_name)) continue;

        if (!actual_count) {
            flecs_json_memberl(buf, "var_labels");
            flecs_json_array_push(buf);
            actual_count ++;
        }

        ecs_strbuf_list_next(buf);
        flecs_json_label(buf, world, variables[i].entity);
    }

    if (actual_count) {
        flecs_json_array_pop(buf);
    }
}

static
void flecs_json_serialize_iter_result_variable_ids(
    const ecs_iter_t *it,
    ecs_strbuf_t *buf) 
{
    char **variable_names = it->variable_names;
    ecs_var_t *variables = it->variables;
    int32_t var_count = it->variable_count;
    int32_t actual_count = 0;

    for (int i = 0; i < var_count; i ++) {
        const char *var_name = variable_names[i];
        if (flecs_json_skip_variable(var_name)) continue;

        if (!actual_count) {
            flecs_json_memberl(buf, "var_ids");
            flecs_json_array_push(buf);
            actual_count ++;
        }

        ecs_strbuf_list_next(buf);
        flecs_json_number(buf, (double)variables[i].entity);
    }

    if (actual_count) {
        flecs_json_array_pop(buf);
    }
}

static
bool flecs_json_serialize_iter_result_entity_names(
    const ecs_iter_t *it,
    ecs_strbuf_t *buf) 
{
    ecs_assert(it->count != 0, ECS_INTERNAL_ERROR, NULL);

    EcsIdentifier *names = ecs_table_get_id(it->world, it->table, 
        ecs_pair(ecs_id(EcsIdentifier), EcsName), it->offset);
    if (!names) {
        return false;
    }

    int i;
    for (i = 0; i < it->count; i ++) {
        flecs_json_next(buf);
        flecs_json_string(buf, names[i].value);
    }

    return true;
}

static
void flecs_json_serialize_iter_result_entity_ids(
    const ecs_iter_t *it,
    ecs_strbuf_t *buf) 
{
    if (!it->count) {
        return;
    }

    flecs_json_memberl(buf, "entity_ids");
    flecs_json_array_push(buf);

    ecs_entity_t *entities = it->entities;

    int i, count = it->count;
    for (i = 0; i < count; i ++) {
        flecs_json_next(buf);
        flecs_json_number(buf, (double)(uint32_t)entities[i]);
    }

    flecs_json_array_pop(buf);
}

static
void flecs_json_serialize_iter_result_parent(
    const ecs_world_t *world,
    const ecs_iter_t *it,
    ecs_strbuf_t *buf) 
{
    ecs_table_t *table = it->table;
    if (!(table->flags & EcsTableHasChildOf)) {
        return;
    }

    const ecs_table_record_t *tr = flecs_id_record_get_table(
        world->idr_childof_wildcard, it->table);
    if (tr == NULL) {
        return;
    }

    ecs_id_t id = table->type.array[tr->column];
    ecs_entity_t parent = ecs_pair_second(world, id);
    char *path = ecs_get_fullpath(world, parent);
    flecs_json_memberl(buf, "parent");
    flecs_json_string(buf, path);
    ecs_os_free(path);
}

static
void flecs_json_serialize_iter_result_entities(
    const ecs_world_t *world,
    const ecs_iter_t *it,
    ecs_strbuf_t *buf) 
{
    if (!it->count) {
        return;
    }

    flecs_json_serialize_iter_result_parent(world, it, buf);

    flecs_json_memberl(buf, "entities");
    flecs_json_array_push(buf);

    if (!flecs_json_serialize_iter_result_entity_names(it, buf)) {
        ecs_entity_t *entities = it->entities;

        int i, count = it->count;
        for (i = 0; i < count; i ++) {
            flecs_json_next(buf);
            flecs_json_number(buf, (double)(uint32_t)entities[i]);
        }
    }

    flecs_json_array_pop(buf);
}

static
void flecs_json_serialize_iter_result_entity_labels(
    const ecs_iter_t *it,
    ecs_strbuf_t *buf,
    const ecs_json_ser_idr_t *ser_idr)
{
    (void)buf;
    (void)ser_idr;
    if (!it->count) {
        return;
    }

    if (!ser_idr->idr_doc_name) {
        return;
    }

#ifdef FLECS_DOC
    const ecs_table_record_t *tr = flecs_id_record_get_table(
        ser_idr->idr_doc_name, it->table);
    if (tr == NULL) {
        return;
    }

    EcsDocDescription *labels = ecs_table_get_column(
        it->table, tr->column, it->offset);
    ecs_assert(labels != NULL, ECS_INTERNAL_ERROR, NULL);

    flecs_json_memberl(buf, "entity_labels");
    flecs_json_array_push(buf);

    int i;
    for (i = 0; i < it->count; i ++) {
        flecs_json_next(buf);
        flecs_json_string(buf, labels[i].value);
    }

    flecs_json_array_pop(buf);
#endif
}

static
void flecs_json_serialize_iter_result_colors(
    const ecs_iter_t *it,
    ecs_strbuf_t *buf,
    const ecs_json_ser_idr_t *ser_idr) 
{
    (void)buf;
    (void)ser_idr;

    if (!it->count) {
        return;
    }

#ifdef FLECS_DOC
    if (!ser_idr->idr_doc_color) {
        return;
    }

    const ecs_table_record_t *tr = flecs_id_record_get_table(
        ser_idr->idr_doc_color, it->table);
    if (tr == NULL) {
        return;
    }

    EcsDocDescription *colors = ecs_table_get_column(
        it->table, tr->column, it->offset);
    ecs_assert(colors != NULL, ECS_INTERNAL_ERROR, NULL);

    flecs_json_memberl(buf, "colors");
    flecs_json_array_push(buf);

    int i;
    for (i = 0; i < it->count; i ++) {
        flecs_json_next(buf);
        flecs_json_string(buf, colors[i].value);
    }

    flecs_json_array_pop(buf);
#endif
}

static
int flecs_json_serialize_iter_result_values(
    const ecs_world_t *world,
    const ecs_iter_t *it,
    ecs_strbuf_t *buf) 
{
    if (!it->ptrs || (it->flags & EcsIterNoData)) {
        return 0;
    }

    flecs_json_memberl(buf, "values");
    flecs_json_array_push(buf);

    int32_t i, term_count = it->field_count;
    for (i = 0; i < term_count; i ++) {
        ecs_strbuf_list_next(buf);

        const void *ptr = NULL;
        if (it->ptrs) {
            ptr = it->ptrs[i];
        }

        if (!ptr) {
            /* No data in column. Append 0 if this is not an optional term */
            if (ecs_field_is_set(it, i + 1)) {
                ecs_strbuf_appendch(buf, '0');
                continue;
            }
        }

        if (ecs_field_is_writeonly(it, i + 1)) {
            ecs_strbuf_appendch(buf, '0');
            continue;
        }

        /* Get component id (can be different in case of pairs) */
        ecs_entity_t type = ecs_get_typeid(world, it->ids[i]);
        if (!type) {
            /* Odd, we have a ptr but no Component? Not the place of the
             * serializer to complain about that. */
            ecs_strbuf_appendch(buf, '0');
            continue;
        }

        const EcsComponent *comp = ecs_get(world, type, EcsComponent);
        if (!comp) {
            /* Also odd, typeid but not a component? */
            ecs_strbuf_appendch(buf, '0');
            continue;
        }

        const EcsMetaTypeSerialized *ser = ecs_get(
            world, type, EcsMetaTypeSerialized);
        if (!ser) {
            /* Not odd, component just has no reflection data */
            ecs_strbuf_appendch(buf, '0');
            continue;
        }

        /* If term is not set, append empty array. This indicates that the term
         * could have had data but doesn't */
        if (!ecs_field_is_set(it, i + 1)) {
            ecs_assert(ptr == NULL, ECS_INTERNAL_ERROR, NULL);
            flecs_json_array_push(buf);
            flecs_json_array_pop(buf);
            continue;
        }

        if (ecs_field_is_self(it, i + 1)) {
            int32_t count = it->count;
            if (array_to_json_buf_w_type_data(world, ptr, count, buf, comp, ser)) {
                return -1;
            }
        } else {
            if (array_to_json_buf_w_type_data(world, ptr, 0, buf, comp, ser)) {
                return -1;
            }
        }
    }

    flecs_json_array_pop(buf);

    return 0;
}

static
int flecs_json_serialize_iter_result_columns(
    const ecs_world_t *world,
    const ecs_iter_t *it,
    ecs_strbuf_t *buf)
{
    ecs_table_t *table = it->table;
    if (!table || !table->storage_table) {
        return 0;
    }

    flecs_json_memberl(buf, "values");
    flecs_json_array_push(buf);

    ecs_type_t *type = &table->type;
    int32_t *storage_map = table->storage_map;
    ecs_assert(storage_map != NULL, ECS_INTERNAL_ERROR, NULL);

    for (int i = 0; i < type->count; i ++) {
        int32_t storage_column = -1;
        if (storage_map) {
            storage_column = storage_map[i];
        }

        ecs_strbuf_list_next(buf);

        if (storage_column == -1) {
            ecs_strbuf_appendch(buf, '0');
            continue;
        }

        ecs_entity_t typeid = table->type_info[storage_column]->component;
        if (!typeid) {
            ecs_strbuf_appendch(buf, '0');
            continue;
        }

        const EcsComponent *comp = ecs_get(world, typeid, EcsComponent);
        if (!comp) {
            ecs_strbuf_appendch(buf, '0');
            continue;
        }

        const EcsMetaTypeSerialized *ser = ecs_get(
            world, typeid, EcsMetaTypeSerialized);
        if (!ser) {
            ecs_strbuf_appendch(buf, '0');
            continue;
        }

        void *ptr = ecs_vec_first(&table->data.columns[storage_column]);
        if (array_to_json_buf_w_type_data(world, ptr, it->count, buf, comp, ser)) {
            return -1;
        }
    }

    flecs_json_array_pop(buf);

    return 0;
}

static
int flecs_json_serialize_iter_result(
    const ecs_world_t *world, 
    const ecs_iter_t *it, 
    ecs_strbuf_t *buf,
    const ecs_iter_to_json_desc_t *desc,
    const ecs_json_ser_idr_t *ser_idr) 
{
    flecs_json_next(buf);
    flecs_json_object_push(buf);

    /* Each result can be matched with different component ids. Add them to
     * the result so clients know with which component an entity was matched */
    if (desc && desc->serialize_table) {
        flecs_json_serialize_iter_result_table_type(world, it, buf);
    } else {
        if (!desc || desc->serialize_ids) {
            flecs_json_serialize_iter_result_ids(world, it, buf);
        }
    }

    /* Include information on which entity the term is matched with */
    if (!desc || (desc->serialize_sources && !desc->serialize_table)) {
        flecs_json_serialize_iter_result_sources(world, it, buf);
    }

    /* Write variable values for current result */
    if (!desc || desc->serialize_variables) {
        flecs_json_serialize_iter_result_variables(world, it, buf);
    }

    /* Write labels for variables */
    if (desc && desc->serialize_variable_labels) {
        flecs_json_serialize_iter_result_variable_labels(world, it, buf);
    }

    /* Write ids for variables */
    if (desc && desc->serialize_variable_ids) {
        flecs_json_serialize_iter_result_variable_ids(it, buf);
    }

    /* Include information on which terms are set, to support optional terms */
    if (!desc || (desc->serialize_is_set && !desc->serialize_table)) {
        flecs_json_serialize_iter_result_is_set(it, buf);
    }

    /* Write entity ids for current result (for queries with This terms) */
    if (!desc || desc->serialize_entities) {
        flecs_json_serialize_iter_result_entities(world, it, buf);
    }

    /* Write ids for entities */
    if (desc && desc->serialize_entity_ids) {
        flecs_json_serialize_iter_result_entity_ids(it, buf);
    }

    /* Write labels for entities */
    if (desc && desc->serialize_entity_labels) {
        flecs_json_serialize_iter_result_entity_labels(it, buf, ser_idr);
    }

    /* Write colors for entities */
    if (desc && desc->serialize_colors) {
        flecs_json_serialize_iter_result_colors(it, buf, ser_idr);
    }

    /* Serialize component values */
    if (desc && desc->serialize_table) {
        if (flecs_json_serialize_iter_result_columns(world, it, buf)) {
            return -1;
        }
    } else {
        if (!desc || desc->serialize_values) {
            if (flecs_json_serialize_iter_result_values(world, it, buf)) {
                return -1;
            }
        }
    }

    /* Add "alerts": true member if table has entities with active alerts */
#ifdef FLECS_ALERTS
    if (it->table && (ecs_id(EcsAlertsActive) != 0)) {
        /* Only add field if alerts addon is imported */
        if (ecs_table_has_id(world, it->table, ecs_id(EcsAlertsActive))) {
            flecs_json_memberl(buf, "alerts");
            flecs_json_true(buf);
        }
    }
#endif

    flecs_json_object_pop(buf);

    return 0;
}

int ecs_iter_to_json_buf(
    const ecs_world_t *world,
    ecs_iter_t *it,
    ecs_strbuf_t *buf,
    const ecs_iter_to_json_desc_t *desc)
{
    ecs_time_t duration = {0};
    if (desc && desc->measure_eval_duration) {
        ecs_time_measure(&duration);
    }

    flecs_json_object_push(buf);

    /* Serialize component ids of the terms (usually provided by query) */
    if (!desc || desc->serialize_term_ids) {
        flecs_json_serialize_iter_ids(world, it, buf);
    }

    /* Serialize type info if enabled */
    if (desc && desc->serialize_type_info) {
        flecs_json_serialize_type_info(world, it, buf);
    }

    /* Serialize variable names, if iterator has any */
    flecs_json_serialize_iter_variables(it, buf);

    /* Serialize results */
    flecs_json_memberl(buf, "results");
    flecs_json_array_push(buf);

    /* Use instancing for improved performance */
    ECS_BIT_SET(it->flags, EcsIterIsInstanced);

    /* If serializing entire table, don't bother letting the iterator populate
     * data fields as we'll be iterating all columns. */
    if (desc && desc->serialize_table) {
        ECS_BIT_SET(it->flags, EcsIterNoData);
    }

    /* Cache id record for flecs.doc ids */
    ecs_json_ser_idr_t ser_idr = {NULL, NULL};
#ifdef FLECS_DOC
    ser_idr.idr_doc_name = flecs_id_record_get(world, 
        ecs_pair_t(EcsDocDescription, EcsName));
    ser_idr.idr_doc_color = flecs_id_record_get(world, 
        ecs_pair_t(EcsDocDescription, EcsDocColor));
#endif

    ecs_iter_next_action_t next = it->next;
    while (next(it)) {
        if (flecs_json_serialize_iter_result(world, it, buf, desc, &ser_idr)) {
            ecs_strbuf_reset(buf);
            ecs_iter_fini(it);
            return -1;
        }
    }

    flecs_json_array_pop(buf);

    if (desc && desc->measure_eval_duration) {
        double dt = ecs_time_measure(&duration);
        flecs_json_memberl(buf, "eval_duration");
        flecs_json_number(buf, dt);
    }

    flecs_json_object_pop(buf);

    return 0;
}

char* ecs_iter_to_json(
    const ecs_world_t *world,
    ecs_iter_t *it,
    const ecs_iter_to_json_desc_t *desc)
{
    ecs_strbuf_t buf = ECS_STRBUF_INIT;

    if (ecs_iter_to_json_buf(world, it, &buf, desc)) {
        ecs_strbuf_reset(&buf);
        return NULL;
    }

    return ecs_strbuf_get(&buf);
}

int ecs_world_to_json_buf(
    ecs_world_t *world,
    ecs_strbuf_t *buf_out,
    const ecs_world_to_json_desc_t *desc)
{
    ecs_filter_t f = ECS_FILTER_INIT;
    ecs_filter_desc_t filter_desc = {0};
    filter_desc.storage = &f;

    if (desc && desc->serialize_builtin && desc->serialize_modules) {
        filter_desc.terms[0].id = EcsAny;
    } else {
        bool serialize_builtin = desc && desc->serialize_builtin;
        bool serialize_modules = desc && desc->serialize_modules;
        int32_t term_id = 0;

        if (!serialize_builtin) {
            filter_desc.terms[term_id].id = ecs_pair(EcsChildOf, EcsFlecs);
            filter_desc.terms[term_id].oper = EcsNot;
            filter_desc.terms[term_id].src.flags = EcsSelf | EcsParent;
            term_id ++;
        }
        if (!serialize_modules) {
            filter_desc.terms[term_id].id = EcsModule;
            filter_desc.terms[term_id].oper = EcsNot;
            filter_desc.terms[term_id].src.flags = EcsSelf | EcsParent;
        }
    }

    if (ecs_filter_init(world, &filter_desc) == NULL) {
        return -1;
    }

    ecs_iter_t it = ecs_filter_iter(world, &f);
    ecs_iter_to_json_desc_t json_desc = { 
        .serialize_table = true,
        .serialize_entities = true
    };

    int ret = ecs_iter_to_json_buf(world, &it, buf_out, &json_desc);
    ecs_filter_fini(&f);
    return ret;
}

char* ecs_world_to_json(
    ecs_world_t *world,
    const ecs_world_to_json_desc_t *desc)
{
    ecs_strbuf_t buf = ECS_STRBUF_INIT;

    if (ecs_world_to_json_buf(world, &buf, desc)) {
        ecs_strbuf_reset(&buf);
        return NULL;
    }

    return ecs_strbuf_get(&buf);
}

#endif

/**
 * @file json/serialize_type_info.c
 * @brief Serialize type (reflection) information to JSON.
 */


#ifdef FLECS_JSON

static
int json_typeinfo_ser_type(
    const ecs_world_t *world,
    ecs_entity_t type,
    ecs_strbuf_t *buf);

static
int json_typeinfo_ser_primitive(
    ecs_primitive_kind_t kind,
    ecs_strbuf_t *str) 
{
    switch(kind) {
    case EcsBool:
        flecs_json_string(str, "bool");
        break;
    case EcsChar:
    case EcsString:
        flecs_json_string(str, "text");
        break;
    case EcsByte:
        flecs_json_string(str, "byte");
        break;
    case EcsU8:
    case EcsU16:
    case EcsU32:
    case EcsU64:
    case EcsI8:
    case EcsI16:
    case EcsI32:
    case EcsI64:
    case EcsIPtr:
    case EcsUPtr:
        flecs_json_string(str, "int");
        break;
    case EcsF32:
    case EcsF64:
        flecs_json_string(str, "float");
        break;
    case EcsEntity:
        flecs_json_string(str, "entity");
        break;
    default:
        return -1;
    }

    return 0;
}

static
void json_typeinfo_ser_constants(
    const ecs_world_t *world,
    ecs_entity_t type,
    ecs_strbuf_t *str)
{
    ecs_iter_t it = ecs_term_iter(world, &(ecs_term_t) {
        .id = ecs_pair(EcsChildOf, type)
    });

    while (ecs_term_next(&it)) {
        int32_t i, count = it.count;
        for (i = 0; i < count; i ++) {
            flecs_json_next(str);
            flecs_json_string(str, ecs_get_name(world, it.entities[i]));
        }
    }
}

static
void json_typeinfo_ser_enum(
    const ecs_world_t *world,
    ecs_entity_t type,
    ecs_strbuf_t *str)
{
    ecs_strbuf_list_appendstr(str, "\"enum\"");
    json_typeinfo_ser_constants(world, type, str);
}

static
void json_typeinfo_ser_bitmask(
    const ecs_world_t *world,
    ecs_entity_t type,
    ecs_strbuf_t *str)
{
    ecs_strbuf_list_appendstr(str, "\"bitmask\"");
    json_typeinfo_ser_constants(world, type, str);
}

static
int json_typeinfo_ser_array(
    const ecs_world_t *world,
    ecs_entity_t elem_type,
    int32_t count,
    ecs_strbuf_t *str)
{
    ecs_strbuf_list_appendstr(str, "\"array\"");

    flecs_json_next(str);
    if (json_typeinfo_ser_type(world, elem_type, str)) {
        goto error;
    }

    ecs_strbuf_list_append(str, "%u", count);
    return 0;
error:
    return -1;
}

static
int json_typeinfo_ser_array_type(
    const ecs_world_t *world,
    ecs_entity_t type,
    ecs_strbuf_t *str)
{
    const EcsArray *arr = ecs_get(world, type, EcsArray);
    ecs_assert(arr != NULL, ECS_INTERNAL_ERROR, NULL);
    if (json_typeinfo_ser_array(world, arr->type, arr->count, str)) {
        goto error;
    }

    return 0;
error:
    return -1;
}

static
int json_typeinfo_ser_vector(
    const ecs_world_t *world,
    ecs_entity_t type,
    ecs_strbuf_t *str)
{
    const EcsVector *arr = ecs_get(world, type, EcsVector);
    ecs_assert(arr != NULL, ECS_INTERNAL_ERROR, NULL);

    ecs_strbuf_list_appendstr(str, "\"vector\"");

    flecs_json_next(str);
    if (json_typeinfo_ser_type(world, arr->type, str)) {
        goto error;
    }

    return 0;
error:
    return -1;
}

/* Serialize unit information */
static
int json_typeinfo_ser_unit(
    const ecs_world_t *world,
    ecs_strbuf_t *str,
    ecs_entity_t unit) 
{
    flecs_json_memberl(str, "unit");
    flecs_json_path(str, world, unit);

    const EcsUnit *uptr = ecs_get(world, unit, EcsUnit);
    if (uptr) {
        if (uptr->symbol) {
            flecs_json_memberl(str, "symbol");
            flecs_json_string(str, uptr->symbol);
        }
        ecs_entity_t quantity = ecs_get_target(world, unit, EcsQuantity, 0);
        if (quantity) {
            flecs_json_memberl(str, "quantity");
            flecs_json_path(str, world, quantity);
        }
    }

    return 0;
}

/* Forward serialization to the different type kinds */
static
int json_typeinfo_ser_type_op(
    const ecs_world_t *world,
    ecs_meta_type_op_t *op, 
    ecs_strbuf_t *str) 
{
    if (op->kind == EcsOpOpaque) {
        const EcsOpaque *ct = ecs_get(world, op->type, 
            EcsOpaque);
        ecs_assert(ct != NULL, ECS_INTERNAL_ERROR, NULL);
        return json_typeinfo_ser_type(world, ct->as_type, str);
    }

    flecs_json_array_push(str);

    switch(op->kind) {
    case EcsOpPush:
    case EcsOpPop:
        /* Should not be parsed as single op */
        ecs_throw(ECS_INVALID_PARAMETER, NULL);
        break;
    case EcsOpEnum:
        json_typeinfo_ser_enum(world, op->type, str);
        break;
    case EcsOpBitmask:
        json_typeinfo_ser_bitmask(world, op->type, str);
        break;
    case EcsOpArray:
        json_typeinfo_ser_array_type(world, op->type, str);
        break;
    case EcsOpVector:
        json_typeinfo_ser_vector(world, op->type, str);
        break;
    case EcsOpOpaque:
        /* Can't happen, already handled above */
        ecs_abort(ECS_INTERNAL_ERROR, NULL);
        break;
    default:
        if (json_typeinfo_ser_primitive( 
            flecs_json_op_to_primitive_kind(op->kind), str))
        {
            /* Unknown operation */
            ecs_throw(ECS_INTERNAL_ERROR, NULL);
            return -1;
        }
        break;
    }

    ecs_entity_t unit = op->unit;
    if (unit) {
        flecs_json_next(str);
        flecs_json_next(str);

        flecs_json_object_push(str);
        json_typeinfo_ser_unit(world, str, unit);
        flecs_json_object_pop(str);
    }

    flecs_json_array_pop(str);

    return 0;
error:
    return -1;
}

/* Iterate over a slice of the type ops array */
static
int json_typeinfo_ser_type_ops(
    const ecs_world_t *world,
    ecs_meta_type_op_t *ops,
    int32_t op_count,
    ecs_strbuf_t *str) 
{
    for (int i = 0; i < op_count; i ++) {
        ecs_meta_type_op_t *op = &ops[i];

        if (op != ops) {
            if (op->name) {
                flecs_json_member(str, op->name);
            }
        }

        int32_t elem_count = op->count;
        if (elem_count > 1) {
            flecs_json_array_push(str);
            json_typeinfo_ser_array(world, op->type, op->count, str);
            flecs_json_array_pop(str);
            i += op->op_count - 1;
            continue;
        }
        
        switch(op->kind) {
        case EcsOpPush:
            flecs_json_object_push(str);
            break;
        case EcsOpPop:
            flecs_json_object_pop(str);
            break;
        default:
            if (json_typeinfo_ser_type_op(world, op, str)) {
                goto error;
            }
            break;
        }
    }

    return 0;
error:
    return -1;
}

static
int json_typeinfo_ser_type(
    const ecs_world_t *world,
    ecs_entity_t type,
    ecs_strbuf_t *buf)
{
    const EcsComponent *comp = ecs_get(world, type, EcsComponent);
    if (!comp) {
        ecs_strbuf_appendch(buf, '0');
        return 0;
    }

    const EcsMetaTypeSerialized *ser = ecs_get(
        world, type, EcsMetaTypeSerialized);
    if (!ser) {
        ecs_strbuf_appendch(buf, '0');
        return 0;
    }

    ecs_meta_type_op_t *ops = ecs_vec_first_t(&ser->ops, ecs_meta_type_op_t);
    int32_t count = ecs_vec_count(&ser->ops);

    return json_typeinfo_ser_type_ops(world, ops, count, buf);
}

int ecs_type_info_to_json_buf(
    const ecs_world_t *world,
    ecs_entity_t type,
    ecs_strbuf_t *buf)
{
    return json_typeinfo_ser_type(world, type, buf);
}

char* ecs_type_info_to_json(
    const ecs_world_t *world,
    ecs_entity_t type)
{
    ecs_strbuf_t str = ECS_STRBUF_INIT;

    if (ecs_type_info_to_json_buf(world, type, &str) != 0) {
        ecs_strbuf_reset(&str);
        return NULL;
    }

    return ecs_strbuf_get(&str);
}

#endif

/**
 * @file json/deserialize.c
 * @brief Deserialize JSON strings into (component) values.
 */

#include <ctype.h>

#ifdef FLECS_JSON

static
const char* flecs_json_parse_path(
    const ecs_world_t *world,
    const char *json,
    char *token,
    ecs_entity_t *out,
    const ecs_from_json_desc_t *desc)
{
    json = flecs_json_expect(json, JsonString, token, desc);
    if (!json) {
        goto error;
    }

    ecs_entity_t result = ecs_lookup_fullpath(world, token);
    if (!result) {
        ecs_parser_error(desc->name, desc->expr, json - desc->expr, 
            "unresolved identifier '%s'", token);
        goto error;
    }

    *out = result;

    return json;
error:
    return NULL;
}

const char* ecs_ptr_from_json(
    const ecs_world_t *world,
    ecs_entity_t type,
    void *ptr,
    const char *json,
    const ecs_from_json_desc_t *desc)
{
    ecs_json_token_t token_kind = 0;
    char token_buffer[ECS_MAX_TOKEN_SIZE], t_lah[ECS_MAX_TOKEN_SIZE];
    char *token = token_buffer;
    int depth = 0;

    const char *name = NULL;
    const char *expr = NULL;

    ecs_meta_cursor_t cur = ecs_meta_cursor(world, type, ptr);
    if (cur.valid == false) {
        return NULL;
    }

    if (desc) {
        name = desc->name;
        expr = desc->expr;
        cur.lookup_action = desc->lookup_action;
        cur.lookup_ctx = desc->lookup_ctx;
    }

    while ((json = flecs_json_parse(json, &token_kind, token))) {
        if (token_kind == JsonLargeString) {
            ecs_strbuf_t large_token = ECS_STRBUF_INIT;
            json = flecs_json_parse_large_string(json, &large_token);
            if (!json) {
                break;
            }

            token = ecs_strbuf_get(&large_token);
            token_kind = JsonString;
        }

        if (token_kind == JsonObjectOpen) {
            depth ++;
            if (ecs_meta_push(&cur) != 0) {
                goto error;
            }

            if (ecs_meta_is_collection(&cur)) {
                ecs_parser_error(name, expr, json - expr, "expected '['");
                return NULL;
            }
        } else if (token_kind == JsonObjectClose) {
            depth --;

            if (ecs_meta_is_collection(&cur)) {
                ecs_parser_error(name, expr, json - expr, "expected ']'");
                return NULL;
            }

            if (ecs_meta_pop(&cur) != 0) {
                goto error;
            }
        } else if (token_kind == JsonArrayOpen) {
            depth ++;
            if (ecs_meta_push(&cur) != 0) {
                goto error;
            }

            if (!ecs_meta_is_collection(&cur)) {
                ecs_parser_error(name, expr, json - expr, "expected '{'");
                return NULL;
            }
        } else if (token_kind == JsonArrayClose) {
            depth --;

            if (!ecs_meta_is_collection(&cur)) {
                ecs_parser_error(name, expr, json - expr, "expected '}'");
                return NULL;
            }

            if (ecs_meta_pop(&cur) != 0) {
                goto error;
            }
        } else if (token_kind == JsonComma) {
            if (ecs_meta_next(&cur) != 0) {
                goto error;
            }
        } else if (token_kind == JsonNull) {
            if (ecs_meta_set_null(&cur) != 0) {
                goto error;
            }
        } else if (token_kind == JsonString) {
            const char *lah = flecs_json_parse(
                json, &token_kind, t_lah);
            if (token_kind == JsonColon) {
                /* Member assignment */
                json = lah;
                if (ecs_meta_dotmember(&cur, token) != 0) {
                    goto error;
                }
            } else {
                if (ecs_meta_set_string(&cur, token) != 0) {
                    goto error;
                }
            }
        } else if (token_kind == JsonNumber) {
            double number = atof(token);
            if (ecs_meta_set_float(&cur, number) != 0) {
                goto error;
            }
        } else if (token_kind == JsonNull) {
            if (ecs_meta_set_null(&cur) != 0) {
                goto error;
            }
        } else if (token_kind == JsonTrue) {
            if (ecs_meta_set_bool(&cur, true) != 0) {
                goto error;
            }
        } else if (token_kind == JsonFalse) {
            if (ecs_meta_set_bool(&cur, false) != 0) {
                goto error;
            }
        } else {
            goto error;
        }

        if (token != token_buffer) {
            ecs_os_free(token);
            token = token_buffer;
        }

        if (!depth) {
            break;
        }
    }

    return json;
error:
    return NULL;
}

const char* ecs_entity_from_json(
    ecs_world_t *world,
    ecs_entity_t e,
    const char *json,
    const ecs_from_json_desc_t *desc_param)
{
    ecs_json_token_t token_kind = 0;
    char token[ECS_MAX_TOKEN_SIZE];

    ecs_from_json_desc_t desc = {0};

    const char *name = NULL, *expr = json, *ids = NULL, *values = NULL, *lah;
    if (desc_param) {
        desc = *desc_param;
    }

    json = flecs_json_expect(json, JsonObjectOpen, token, &desc);
    if (!json) {
        goto error;
    }

    lah = flecs_json_parse(json, &token_kind, token);
    if (!lah) {
        goto error;
    }

    if (token_kind == JsonObjectClose) {
        return lah;
    }

    json = flecs_json_expect_member(json, token, &desc);
    if (!json) {
        return NULL;
    }

    if (!ecs_os_strcmp(token, "path")) {
        json = flecs_json_expect(json, JsonString, token, &desc);
        if (!json) {
            goto error;
        }

        ecs_add_fullpath(world, e, token);

        json = flecs_json_parse(json, &token_kind, token);
        if (!json) {
            goto error;
        }

        if (token_kind == JsonObjectClose) {
            return json;
        } else if (token_kind != JsonComma) {
            ecs_parser_error(name, expr, json - expr, "unexpected character");
            goto error;
        }

        json = flecs_json_expect_member_name(json, token, "ids", &desc);
        if (!json) {
            goto error;
        }
    } else if (ecs_os_strcmp(token, "ids")) {
        ecs_parser_error(name, expr, json - expr, "expected member 'ids'");
        goto error;
    }

    json = flecs_json_expect(json, JsonArrayOpen, token, &desc);
    if (!json) {
        goto error;
    }

    ids = json;

    json = flecs_json_skip_array(json, token, &desc);
    if (!json) {
        return NULL;
    }

    json = flecs_json_parse(json, &token_kind, token);
    if (token_kind != JsonObjectClose) {
        if (token_kind != JsonComma) {
            ecs_parser_error(name, expr, json - expr, "expected ','");
            goto error;
        }

        json = flecs_json_expect_member_name(json, token, "values", &desc);
        if (!json) {
            goto error;
        }

        json = flecs_json_expect(json, JsonArrayOpen, token, &desc);
        if (!json) {
            goto error;
        }

        values = json;
    }

    do {
        ecs_entity_t first = 0, second = 0, type_id = 0;
        ecs_id_t id;

        ids = flecs_json_parse(ids, &token_kind, token);
        if (!ids) {
            goto error;
        }

        if (token_kind == JsonArrayClose) {
            if (values) {
                if (values[0] != ']') {
                    ecs_parser_error(name, expr, values - expr, "expected ']'");
                    goto error;
                }
                json = ecs_parse_ws_eol(values + 1);
            } else {
                json = ids;
            }

            break;
        } else if (token_kind == JsonArrayOpen) {
            ids = flecs_json_parse_path(world, ids, token, &first, &desc);
            if (!ids) {
                goto error;
            }

            ids = flecs_json_parse(ids, &token_kind, token);
            if (!ids) {
                goto error;
            }

            if (token_kind == JsonComma) {
                /* Id is a pair*/
                ids = flecs_json_parse_path(world, ids, token, &second, &desc);
                if (!ids) {
                    goto error;
                }

                ids = flecs_json_expect(ids, JsonArrayClose, token, &desc);
                if (!ids) {
                    goto error;
                }
            } else if (token_kind != JsonArrayClose) {
                ecs_parser_error(name, expr, ids - expr, "expected ',' or ']'");
                goto error;
            }

            lah = flecs_json_parse(ids, &token_kind, token);
            if (!lah) {
                goto error;
            }

            if (token_kind == JsonComma) {
                ids = lah;
            } else if (token_kind != JsonArrayClose) {
                ecs_parser_error(name, expr, lah - expr, "expected ',' or ']'");
                goto error;
            }
        } else {
            ecs_parser_error(name, expr, lah - expr, "expected '[' or ']'");
            goto error;
        }

        if (second) {
            id = ecs_pair(first, second);
            type_id = ecs_get_typeid(world, id);
            if (!type_id) {
                ecs_parser_error(name, expr, ids - expr, "id is not a type");
                goto error;
            }
        } else {
            id = first;
            type_id = first;
        }

        /* Get mutable pointer */
        void *comp_ptr = ecs_get_mut_id(world, e, id);
        if (!comp_ptr) {
            char *idstr = ecs_id_str(world, id);
            ecs_parser_error(name, expr, json - expr, 
                "id '%s' is not a valid component", idstr);
            ecs_os_free(idstr);
            goto error;
        }

        if (values) {
            ecs_from_json_desc_t parse_desc = {
                .name = name,
                .expr = expr,
            };

            values = ecs_ptr_from_json(
                world, type_id, comp_ptr, values, &parse_desc);
            if (!values) {
                goto error;
            }

            lah = flecs_json_parse(values, &token_kind, token);
            if (!lah) {
                goto error;
            }

            if (token_kind == JsonComma) {
                values = lah;
            } else if (token_kind != JsonArrayClose) {
                ecs_parser_error(name, expr, json - expr, 
                    "expected ',' or ']'");
                goto error;
            } else {
                values = ecs_parse_ws_eol(values);
            }

            ecs_modified_id(world, e, id);
        }
    } while(ids[0]);

    return flecs_json_expect(json, JsonObjectClose, token, &desc);
error:
    return NULL;
}

static
ecs_entity_t flecs_json_new_id(
    ecs_world_t *world,
    ecs_entity_t ser_id)
{
    /* Try to honor low id requirements */
    if (ser_id < FLECS_HI_COMPONENT_ID) {
        return ecs_new_low_id(world);
    } else {
        return ecs_new_id(world);
    }
}

static
ecs_entity_t flecs_json_lookup(
    ecs_world_t *world,
    ecs_entity_t parent,
    const char *name,
    const ecs_from_json_desc_t *desc)
{
    ecs_entity_t scope = 0;
    if (parent) {
        scope = ecs_set_scope(world, parent);
    }
    ecs_entity_t result = desc->lookup_action(world, name, desc->lookup_ctx);
    if (parent) {
        ecs_set_scope(world, scope);
    }
    return result;
}

static
void flecs_json_mark_reserved(
    ecs_map_t *anonymous_ids,
    ecs_entity_t e)
{
    ecs_entity_t *reserved = ecs_map_ensure(anonymous_ids, e);
    ecs_assert(reserved[0] == 0, ECS_INTERNAL_ERROR, NULL);
    reserved[0] = 0;
}

static
bool flecs_json_name_is_anonymous(
    const char *name)
{
    if (isdigit(name[0])) {
        const char *ptr;
        for (ptr = name + 1; *ptr; ptr ++) {
            if (!isdigit(*ptr)) {
                break;
            }
        }
        if (!(*ptr)) {
            return true;
        }
    }
    return false;
}

static
ecs_entity_t flecs_json_ensure_entity(
    ecs_world_t *world,
    const char *name,
    ecs_map_t *anonymous_ids)
{
    ecs_entity_t e = 0;

    if (flecs_json_name_is_anonymous(name)) {
        /* Anonymous entity, find or create mapping to new id */
        ecs_entity_t ser_id = flecs_ito(ecs_entity_t, atoll(name));
        ecs_entity_t *deser_id = ecs_map_get(anonymous_ids, ser_id);
        if (deser_id) {
            if (!deser_id[0]) {
                /* Id is already issued by deserializer, create new id */
                deser_id[0] = flecs_json_new_id(world, ser_id);

                /* Mark new id as reserved */
                flecs_json_mark_reserved(anonymous_ids, deser_id[0]);
            } else {
                /* Id mapping exists */
            }
        } else {
            /* Id has not yet been issued by deserializer, which means it's safe
             * to use. This allows the deserializer to bind to existing 
             * anonymous ids, as they will never be reissued. */
            deser_id = ecs_map_ensure(anonymous_ids, ser_id);
            if (!ecs_exists(world, ser_id) || ecs_is_alive(world, ser_id)) {
                /* Only use existing id if it's alive or doesn't exist yet. The 
                 * id could have been recycled for another entity */
                deser_id[0] = ser_id;
                ecs_ensure(world, ser_id);
            } else {
                /* If id exists and is not alive, create a new id */
                deser_id[0] = flecs_json_new_id(world, ser_id);

                /* Mark new id as reserved */
                flecs_json_mark_reserved(anonymous_ids, deser_id[0]);
            }
        }

        e = deser_id[0];
    } else {
        e = ecs_lookup_path_w_sep(world, 0, name, ".", NULL, false);
        if (!e) {
            e = ecs_entity(world, { .name = name });
            flecs_json_mark_reserved(anonymous_ids, e);
        }
    }

    return e;
}

static
ecs_table_t* flecs_json_parse_table(
    ecs_world_t *world,
    const char *json,
    char *token,
    const ecs_from_json_desc_t *desc)
{
    ecs_json_token_t token_kind = 0;
    ecs_table_t *table = NULL;

    do {
        ecs_id_t id = 0;
        json = flecs_json_expect(json, JsonArrayOpen, token, desc);
        if (!json) {
            goto error;
        }

        json = flecs_json_expect(json, JsonString, token, desc);
        if (!json) {
            goto error;
        }

        ecs_entity_t first = flecs_json_lookup(world, 0, token, desc);
        if (!first) {
            goto error;
        }

        json = flecs_json_parse(json, &token_kind, token);
        if (token_kind == JsonComma) {
            json = flecs_json_expect(json, JsonString, token, desc);
            if (!json) {
                goto error;
            }

            ecs_entity_t second = flecs_json_lookup(world, 0, token, desc);
            if (!second) {
                goto error;
            }

            id = ecs_pair(first, second);

            json = flecs_json_expect(json, JsonArrayClose, token, desc);
            if (!json) {
                goto error;
            }
        } else if (token_kind == JsonArrayClose) {
            id = first;
        } else {
            ecs_parser_error(desc->name, desc->expr, json - desc->expr, 
                "expected ',' or ']");
            goto error;
        }

        table = ecs_table_add_id(world, table, id);
        if (!table) {
            goto error;
        }

        const char *lah = flecs_json_parse(json, &token_kind, token);
        if (token_kind == JsonComma) {
            json = lah;
        } else if (token_kind == JsonArrayClose) {
            break;
        } else {
            ecs_parser_error(desc->name, desc->expr, json - desc->expr, 
                "expected ',' or ']'");
            goto error;
        }
    } while (json[0]);

    return table;
error:
    return NULL;
}

static
int flecs_json_parse_entities(
    ecs_world_t *world,
    ecs_allocator_t *a,
    ecs_table_t *table,
    ecs_entity_t parent,
    const char *json,
    char *token,
    ecs_vec_t *records,
    const ecs_from_json_desc_t *desc)
{
    char name_token[ECS_MAX_TOKEN_SIZE];
    ecs_json_token_t token_kind = 0;
    ecs_vec_clear(records);

    do {
        json = flecs_json_parse(json, &token_kind, name_token);
        if (!json) {
            goto error;
        }
        if ((token_kind != JsonNumber) && (token_kind != JsonString)) {
            ecs_parser_error(desc->name, desc->expr, json - desc->expr, 
                "expected number or string");
            goto error;
        }

        ecs_entity_t e = flecs_json_lookup(world, parent, name_token, desc);
        ecs_record_t *r = flecs_entities_try(world, e);
        ecs_assert(r != NULL, ECS_INTERNAL_ERROR, NULL);

        if (r->table != table) {
            bool cleared = false;
            if (r->table) {
                ecs_commit(world, e, r, r->table, NULL, &r->table->type);
                cleared = true;
            }
            ecs_commit(world, e, r, table, &table->type, NULL);
            if (cleared) {
                char *entity_name = strrchr(name_token, '.');
                if (entity_name) {
                    entity_name ++;
                } else {
                    entity_name = name_token;
                }
                if (!flecs_json_name_is_anonymous(entity_name)) {
                    ecs_set_name(world, e, entity_name);
                }
            }
        }

        ecs_assert(table == r->table, ECS_INTERNAL_ERROR, NULL);
        ecs_record_t** elem = ecs_vec_append_t(a, records, ecs_record_t*);
        *elem = r;

        json = flecs_json_parse(json, &token_kind, token);
        if (token_kind == JsonArrayClose) {
            break;
        } else if (token_kind != JsonComma) {
            ecs_parser_error(desc->name, desc->expr, json - desc->expr, 
                "expected ',' or ']'");
            goto error;
        }
    } while(json[0]);

    return 0;
error:
    return -1;
}

static
const char* flecs_json_parse_column(
    ecs_world_t *world,
    ecs_table_t *table,
    int32_t column,
    const char *json,
    char *token,
    ecs_vec_t *records,
    const ecs_from_json_desc_t *desc)
{
    if (!table->storage_table) {
        ecs_parser_error(desc->name, desc->expr, json - desc->expr, 
            "table has no components");
        goto error;
    }

    if (column >= table->type.count) {
        ecs_parser_error(desc->name, desc->expr, json - desc->expr, 
            "more value arrays than component columns in table");
        goto error;
    }

    int32_t data_column = table->storage_map[column];
    if (data_column == -1) {
        char *table_str = ecs_table_str(world, table);
        ecs_parser_error(desc->name, desc->expr, json - desc->expr, 
            "values provided for tag at column %d of table [%s]",   
                column, table_str);

        ecs_os_free(table_str);
        goto error;
    }

    ecs_json_token_t token_kind = 0;
    ecs_vec_t *data = &table->data.columns[data_column];
    ecs_type_info_t *ti = table->type_info[data_column];
    ecs_size_t size = ti->size;
    ecs_entity_t type = ti->component;
    ecs_record_t **record_array = ecs_vec_first_t(records, ecs_record_t*);
    int32_t entity = 0;

    do {
        ecs_record_t *r = record_array[entity];
        int32_t row = ECS_RECORD_TO_ROW(r->row);
        ecs_assert(ecs_vec_get_t(
            &table->data.records, ecs_record_t*, row)[0] == r,
                ECS_INTERNAL_ERROR, NULL);

        void *ptr = ecs_vec_get(data, size, row);
        ecs_assert(ptr != NULL, ECS_INTERNAL_ERROR, NULL);

        json = ecs_ptr_from_json(world, type, ptr, json, desc);
        if (!json) {
            break;
        }

        json = flecs_json_parse(json, &token_kind, token);
        if (token_kind == JsonArrayClose) {
            break;
        } else if (token_kind != JsonComma) {
            ecs_parser_error(desc->name, desc->expr, json - desc->expr, 
                "expected ',' or ']'");
        }

        entity ++;
    } while (json[0]);

    return json;
error:
    return NULL;
}

static
const char* flecs_json_parse_values(
    ecs_world_t *world,
    ecs_table_t *table,
    const char *json,
    char *token,
    ecs_vec_t *records,
    ecs_vec_t *columns_set,
    const ecs_from_json_desc_t *desc)
{
    ecs_allocator_t *a = &world->allocator;
    ecs_json_token_t token_kind = 0;
    int32_t column = 0;

    ecs_vec_clear(columns_set);

    do {
        json = flecs_json_parse(json, &token_kind, token);
        if (!json) {
            goto error;
        }

        if (token_kind == JsonArrayClose) {
            break;
        } else if (token_kind == JsonArrayOpen) {
            json = flecs_json_parse_column(world, table, column,
                json, token, records, desc);
            if (!json) {
                goto error;
            }

            ecs_id_t *id_set = ecs_vec_append_t(a, columns_set, ecs_id_t);
            *id_set = table->type.array[column];

            column ++;
        } else if (token_kind == JsonNumber) {
            if (!ecs_os_strcmp(token, "0")) {
                column ++; /* no data */
            } else {
                ecs_parser_error(desc->name, desc->expr, json - desc->expr, 
                    "unexpected number");
                goto error;
            }
        }

        json = flecs_json_parse(json, &token_kind, token);
        if (token_kind == JsonArrayClose) {
            break;
        } else if (token_kind != JsonComma) {
            ecs_parser_error(desc->name, desc->expr, json - desc->expr,
                    "expected ',' or ']'");
            goto error;
        }
    } while (json[0]);

    /* Send OnSet notifications */
    ecs_defer_begin(world);
    ecs_type_t type = { 
        .array = columns_set->array, 
        .count = columns_set->count };

    int32_t table_count = ecs_table_count(table);
    int32_t i, record_count = ecs_vec_count(records);

    /* If the entire table was inserted, send bulk notification */
    if (table_count == ecs_vec_count(records)) {
        flecs_notify_on_set(world, table, 0, ecs_table_count(table), &type, true);
    } else {
        ecs_record_t **rvec = ecs_vec_first_t(records, ecs_record_t*);
        for (i = 0; i < record_count; i ++) {
            ecs_record_t *r = rvec[i];
            int32_t row = ECS_RECORD_TO_ROW(r->row);
            flecs_notify_on_set(world, table, row, 1, &type, true);
        }
    }

    ecs_defer_end(world);

    return json;
error:
    return NULL;
}

static
const char* flecs_json_parse_result(
    ecs_world_t *world,
    ecs_allocator_t *a,
    const char *json,
    char *token,
    ecs_vec_t *records,
    ecs_vec_t *columns_set,
    const ecs_from_json_desc_t *desc)
{
    ecs_json_token_t token_kind = 0;
    const char *ids = NULL, *values = NULL, *entities = NULL;

    json = flecs_json_expect(json, JsonObjectOpen, token, desc);
    if (!json) {
        goto error;
    }

    json = flecs_json_expect_member_name(json, token, "ids", desc);
    if (!json) {
        goto error;
    }

    json = flecs_json_expect(json, JsonArrayOpen, token, desc);
    if (!json) {
        goto error;
    }

    ids = json; /* store start of ids array */

    json = flecs_json_skip_array(json, token, desc);
    if (!json) {
        goto error;
    }

    json = flecs_json_expect(json, JsonComma, token, desc);
    if (!json) {
        goto error;
    }

    json = flecs_json_expect_member(json, token, desc);
    if (!json) {
        goto error;
    }

    ecs_entity_t parent = 0;
    if (!ecs_os_strcmp(token, "parent")) {
        json = flecs_json_expect(json, JsonString, token, desc);
        if (!json) {
            goto error;
        }
        parent = ecs_lookup_fullpath(world, token);

        json = flecs_json_expect(json, JsonComma, token, desc);
        if (!json) {
            goto error;
        }

        json = flecs_json_expect_member(json, token, desc);
        if (!json) {
            goto error;
        }
    }

    if (ecs_os_strcmp(token, "entities")) {
        ecs_parser_error(desc->name, desc->expr, json - desc->expr, 
            "expected 'entities'");
        goto error;
    }

    json = flecs_json_expect(json, JsonArrayOpen, token, desc);
    if (!json) {
        goto error;
    }

    entities = json; /* store start of entity id array */

    json = flecs_json_skip_array(json, token, desc);
    if (!json) {
        goto error;
    }

    json = flecs_json_parse(json, &token_kind, token);
    if (token_kind == JsonComma) {
        json = flecs_json_expect_member_name(json, token, "values", desc);
        if (!json) {
            goto error;
        }

        json = flecs_json_expect(json, JsonArrayOpen, token, desc);
        if (!json) {
            goto error;
        }

        values = json; /* store start of entities array */
    } else if (token_kind != JsonObjectClose) {
        ecs_parser_error(desc->name, desc->expr, json - desc->expr, 
            "expected ',' or '}'");
        goto error;
    }

    /* Find table from ids */
    ecs_table_t *table = flecs_json_parse_table(world, ids, token, desc);
    if (!table) {
        goto error;
    }

    /* Add entities to table */
    if (flecs_json_parse_entities(world, a, table, parent,
        entities, token, records, desc)) 
    {
        goto error;
    }

    /* Parse values */
    if (values) {
        json = flecs_json_parse_values(world, table, values, token, 
            records, columns_set, desc);
        if (!json) {
            goto error;
        }

        json = flecs_json_expect(json, JsonObjectClose, token, desc);
        if (!json) {
            goto error;
        }
    }

    return json;
error:
    return NULL;
}

const char* ecs_world_from_json(
    ecs_world_t *world,
    const char *json,
    const ecs_from_json_desc_t *desc_arg)
{
    ecs_json_token_t token_kind;
    char token[ECS_MAX_TOKEN_SIZE];

    ecs_from_json_desc_t desc = {0};
    ecs_allocator_t *a = &world->allocator;
    ecs_vec_t records;
    ecs_vec_t columns_set;
    ecs_map_t anonymous_ids;
    ecs_vec_init_t(a, &records, ecs_record_t*, 0);
    ecs_vec_init_t(a, &columns_set, ecs_id_t, 0);
    ecs_map_init(&anonymous_ids, a);

    const char *name = NULL, *expr = json, *lah;
    if (desc_arg) {
        desc = *desc_arg;
    }

    if (!desc.lookup_action) {
        desc.lookup_action = (ecs_entity_t(*)(
            const ecs_world_t*, const char*, void*))flecs_json_ensure_entity;
        desc.lookup_ctx = &anonymous_ids;
    }

    json = flecs_json_expect(json, JsonObjectOpen, token, &desc);
    if (!json) {
        goto error;
    }

    json = flecs_json_expect_member_name(json, token, "results", &desc);
    if (!json) {
        goto error;
    }

    json = flecs_json_expect(json, JsonArrayOpen, token, &desc);
    if (!json) {
        goto error;
    }

    lah = flecs_json_parse(json, &token_kind, token);
    if (token_kind == JsonArrayClose) {
        json = lah;
        goto end;
    }

    do {
        json = flecs_json_parse_result(world, a, json, token, 
            &records, &columns_set, &desc);
        if (!json) {
            goto error;
        }

        json = flecs_json_parse(json, &token_kind, token);
        if (token_kind == JsonArrayClose) {
            break;
        } else if (token_kind != JsonComma) {
            ecs_parser_error(name, expr, json - expr,
                    "expected ',' or ']'");
            goto error;
        }
    } while(json && json[0]);

end:
    ecs_vec_fini_t(a, &records, ecs_record_t*);
    ecs_vec_fini_t(a, &columns_set, ecs_id_t);
    ecs_map_fini(&anonymous_ids);

    json = flecs_json_expect(json, JsonObjectClose, token, &desc);
    if (!json) {
        goto error;
    }

    return json;
error:
    ecs_vec_fini_t(a, &records, ecs_record_t*);
    ecs_vec_fini_t(a, &columns_set, ecs_id_t);
    ecs_map_fini(&anonymous_ids);

    return NULL;
}

#endif

/**
 * @file addons/rest.c
 * @brief Rest addon.
 */


#ifdef FLECS_REST

ECS_TAG_DECLARE(EcsRestPlecs);

typedef struct {
    ecs_world_t *world;
    ecs_http_server_t *srv;
    int32_t rc;
} ecs_rest_ctx_t;

/* Global statistics */
int64_t ecs_rest_request_count = 0;
int64_t ecs_rest_entity_count = 0;
int64_t ecs_rest_entity_error_count = 0;
int64_t ecs_rest_query_count = 0;
int64_t ecs_rest_query_error_count = 0;
int64_t ecs_rest_query_name_count = 0;
int64_t ecs_rest_query_name_error_count = 0;
int64_t ecs_rest_query_name_from_cache_count = 0;
int64_t ecs_rest_enable_count = 0;
int64_t ecs_rest_enable_error_count = 0;
int64_t ecs_rest_set_count = 0;
int64_t ecs_rest_set_error_count = 0;
int64_t ecs_rest_delete_count = 0;
int64_t ecs_rest_delete_error_count = 0;
int64_t ecs_rest_world_stats_count = 0;
int64_t ecs_rest_pipeline_stats_count = 0;
int64_t ecs_rest_stats_error_count = 0;

static ECS_COPY(EcsRest, dst, src, {
    ecs_rest_ctx_t *impl = src->impl;
    if (impl) {
        impl->rc ++;
    }

    ecs_os_strset(&dst->ipaddr, src->ipaddr);
    dst->port = src->port;
    dst->impl = impl;
})

static ECS_MOVE(EcsRest, dst, src, {
    *dst = *src;
    src->ipaddr = NULL;
    src->impl = NULL;
})

static ECS_DTOR(EcsRest, ptr, { 
    ecs_rest_ctx_t *impl = ptr->impl;
    if (impl) {
        impl->rc --;
        if (!impl->rc) {
            ecs_http_server_fini(impl->srv);
            ecs_os_free(impl);
        }
    }
    ecs_os_free(ptr->ipaddr);
})

static char *rest_last_err;
static ecs_os_api_log_t rest_prev_log;

static 
void flecs_rest_capture_log(
    int32_t level, 
    const char *file, 
    int32_t line, 
    const char *msg)
{
    (void)file; (void)line;

    if (level < 0) {
        if (rest_prev_log) {
            // Also log to previous log function
            ecs_log_enable_colors(true);
            rest_prev_log(level, file, line, msg);
            ecs_log_enable_colors(false);
        }
    }

    if (!rest_last_err && level < 0) {
        rest_last_err = ecs_os_strdup(msg);
    }
}

static
char* flecs_rest_get_captured_log(void) {
    char *result = rest_last_err;
    rest_last_err = NULL;
    return result;
}

static
void flecs_reply_verror(
    ecs_http_reply_t *reply,
    const char *fmt,
    va_list args)
{
    ecs_strbuf_appendlit(&reply->body, "{\"error\":\"");
    ecs_strbuf_vappend(&reply->body, fmt, args);
    ecs_strbuf_appendlit(&reply->body, "\"}");
}

static
void flecs_reply_error(
    ecs_http_reply_t *reply,
    const char *fmt,
    ...)
{
    va_list args;
    va_start(args, fmt);
    flecs_reply_verror(reply, fmt, args);
    va_end(args);
}

static
void flecs_rest_bool_param(
    const ecs_http_request_t *req,
    const char *name,
    bool *value_out)
{
    const char *value = ecs_http_get_param(req, name);
    if (value) {
        if (!ecs_os_strcmp(value, "true")) {
            value_out[0] = true;
        } else {
            value_out[0] = false;
        }
    }
}

static
void flecs_rest_int_param(
    const ecs_http_request_t *req,
    const char *name,
    int32_t *value_out)
{
    const char *value = ecs_http_get_param(req, name);
    if (value) {
        *value_out = atoi(value);
    }
}

static
void flecs_rest_string_param(
    const ecs_http_request_t *req,
    const char *name,
    char **value_out)
{
    const char *value = ecs_http_get_param(req, name);
    if (value) {
        *value_out = (char*)value;
    }
}

static
void flecs_rest_parse_json_ser_entity_params(
    ecs_entity_to_json_desc_t *desc,
    const ecs_http_request_t *req)
{
    flecs_rest_bool_param(req, "path", &desc->serialize_path);
    flecs_rest_bool_param(req, "label", &desc->serialize_label);
    flecs_rest_bool_param(req, "brief", &desc->serialize_brief);
    flecs_rest_bool_param(req, "link", &desc->serialize_link);
    flecs_rest_bool_param(req, "color", &desc->serialize_color);
    flecs_rest_bool_param(req, "id_labels", &desc->serialize_id_labels);
    flecs_rest_bool_param(req, "base", &desc->serialize_base);
    flecs_rest_bool_param(req, "values", &desc->serialize_values);
    flecs_rest_bool_param(req, "private", &desc->serialize_private);
    flecs_rest_bool_param(req, "type_info", &desc->serialize_type_info);
    flecs_rest_bool_param(req, "alerts", &desc->serialize_alerts);
}

static
void flecs_rest_parse_json_ser_iter_params(
    ecs_iter_to_json_desc_t *desc,
    const ecs_http_request_t *req)
{
    flecs_rest_bool_param(req, "term_ids", &desc->serialize_term_ids);
    flecs_rest_bool_param(req, "ids", &desc->serialize_ids);
    flecs_rest_bool_param(req, "sources", &desc->serialize_sources);
    flecs_rest_bool_param(req, "variables", &desc->serialize_variables);
    flecs_rest_bool_param(req, "is_set", &desc->serialize_is_set);
    flecs_rest_bool_param(req, "values", &desc->serialize_values);
    flecs_rest_bool_param(req, "entities", &desc->serialize_entities);
    flecs_rest_bool_param(req, "entity_labels", &desc->serialize_entity_labels);
    flecs_rest_bool_param(req, "variable_labels", &desc->serialize_variable_labels);
    flecs_rest_bool_param(req, "variable_ids", &desc->serialize_variable_ids);
    flecs_rest_bool_param(req, "colors", &desc->serialize_colors);
    flecs_rest_bool_param(req, "duration", &desc->measure_eval_duration);
    flecs_rest_bool_param(req, "type_info", &desc->serialize_type_info);
    flecs_rest_bool_param(req, "serialize_table", &desc->serialize_table);
}

static
bool flecs_rest_reply_entity(
    ecs_world_t *world,
    const ecs_http_request_t* req,
    ecs_http_reply_t *reply)
{
    char *path = &req->path[7];
    ecs_dbg_2("rest: request entity '%s'", path);

    ecs_os_linc(&ecs_rest_entity_count);

    ecs_entity_t e = ecs_lookup_path_w_sep(
        world, 0, path, "/", NULL, false);
    if (!e) {
        ecs_dbg_2("rest: entity '%s' not found", path);
        flecs_reply_error(reply, "entity '%s' not found", path);
        reply->code = 404;
        ecs_os_linc(&ecs_rest_entity_error_count);
        return true;
    }

    ecs_entity_to_json_desc_t desc = ECS_ENTITY_TO_JSON_INIT;
    flecs_rest_parse_json_ser_entity_params(&desc, req);
    ecs_entity_to_json_buf(world, e, &reply->body, &desc);
    return true;
}

static
bool flecs_rest_reply_world(
    ecs_world_t *world,
    const ecs_http_request_t* req,
    ecs_http_reply_t *reply)
{
    (void)req;
    ecs_world_to_json_buf(world, &reply->body, NULL);
    return true;
}

static
ecs_entity_t flecs_rest_entity_from_path(
    ecs_world_t *world,
    ecs_http_reply_t *reply,
    const char *path)
{
    ecs_entity_t e = ecs_lookup_path_w_sep(
        world, 0, path, "/", NULL, false);
    if (!e) {
        flecs_reply_error(reply, "entity '%s' not found", path);
        reply->code = 404;
    }
    return e;
}

static
bool flecs_rest_set(
    ecs_world_t *world,
    const ecs_http_request_t* req,
    ecs_http_reply_t *reply,
    const char *path)
{
    ecs_os_linc(&ecs_rest_set_count);

    ecs_entity_t e;
    if (!(e = flecs_rest_entity_from_path(world, reply, path))) {
        ecs_os_linc(&ecs_rest_set_error_count);
        return true;
    }

    const char *data = ecs_http_get_param(req, "data");
    ecs_from_json_desc_t desc = {0};
    desc.expr = data;
    desc.name = path;
    if (ecs_entity_from_json(world, e, data, &desc) == NULL) {
        flecs_reply_error(reply, "invalid request");
        reply->code = 400;
        ecs_os_linc(&ecs_rest_set_error_count);
        return true;
    }
    
    return true;
}

static
bool flecs_rest_delete(
    ecs_world_t *world,
    ecs_http_reply_t *reply,
    const char *path)
{
    ecs_os_linc(&ecs_rest_set_count);

    ecs_entity_t e;
    if (!(e = flecs_rest_entity_from_path(world, reply, path))) {
        ecs_os_linc(&ecs_rest_delete_error_count);
        return true;
    }

    ecs_delete(world, e);
    
    return true;
}

static
bool flecs_rest_enable(
    ecs_world_t *world,
    ecs_http_reply_t *reply,
    const char *path,
    bool enable)
{
    ecs_os_linc(&ecs_rest_enable_count);

    ecs_entity_t e;
    if (!(e = flecs_rest_entity_from_path(world, reply, path))) {
        ecs_os_linc(&ecs_rest_enable_error_count);
        return true;
    }

    ecs_enable(world, e, enable);
    
    return true;
}

static
bool flecs_rest_script(
    ecs_world_t *world,
    const ecs_http_request_t* req,
    ecs_http_reply_t *reply)
{
    (void)world;
    (void)req;
    (void)reply;
#ifdef FLECS_PLECS
    const char *data = ecs_http_get_param(req, "data");
    if (!data) {
        flecs_reply_error(reply, "missing data parameter");
        return true;
    }

    bool prev_color = ecs_log_enable_colors(false);
    rest_prev_log = ecs_os_api.log_;
    ecs_os_api.log_ = flecs_rest_capture_log;

    ecs_entity_t script = ecs_script(world, {
        .entity = ecs_entity(world, { .name = "scripts.main" }),
        .str = data
    });

    if (!script) {
        char *err = flecs_rest_get_captured_log();
        char *escaped_err = ecs_astresc('"', err);
        flecs_reply_error(reply, escaped_err);
        ecs_os_linc(&ecs_rest_query_error_count);
        reply->code = 400; /* bad request */
        ecs_os_free(escaped_err);
        ecs_os_free(err);
    }

    ecs_os_api.log_ = rest_prev_log;
    ecs_log_enable_colors(prev_color);

    return true;
#else
    return false;
#endif
}

static
void flecs_rest_reply_set_captured_log(
    ecs_http_reply_t *reply)
{
    char *err = flecs_rest_get_captured_log();
    if (err) {
        char *escaped_err = ecs_astresc('"', err);
        flecs_reply_error(reply, escaped_err);
        reply->code = 400;
        ecs_os_free(escaped_err);
        ecs_os_free(err);
    }
}

static
int flecs_rest_iter_to_reply(
    ecs_world_t *world,
    const ecs_http_request_t* req,
    ecs_http_reply_t *reply,
    ecs_iter_t *it)
{
    ecs_iter_to_json_desc_t desc = ECS_ITER_TO_JSON_INIT;
    flecs_rest_parse_json_ser_iter_params(&desc, req);

    int32_t offset = 0;
    int32_t limit = 1000;

    flecs_rest_int_param(req, "offset", &offset);
    flecs_rest_int_param(req, "limit", &limit);

    if (offset < 0 || limit < 0) {
        flecs_reply_error(reply, "invalid offset/limit parameter");
        reply->code = 400;
        return -1;
    }

    ecs_iter_t pit = ecs_page_iter(it, offset, limit);
    if (ecs_iter_to_json_buf(world, &pit, &reply->body, &desc)) {
        flecs_rest_reply_set_captured_log(reply);
        return -1;
    }

    return 0;
}

static
bool flecs_rest_reply_existing_query(
    ecs_world_t *world,
    const ecs_http_request_t* req,
    ecs_http_reply_t *reply,
    const char *name)
{
    ecs_os_linc(&ecs_rest_query_name_count);

    ecs_entity_t q = ecs_lookup_fullpath(world, name);
    if (!q) {
        flecs_reply_error(reply, "unresolved identifier '%s'", name);
        reply->code = 404;
        ecs_os_linc(&ecs_rest_query_name_error_count);
        return true;
    }

    const EcsPoly *poly = ecs_get_pair(world, q, EcsPoly, EcsQuery);
    if (!poly) {
        flecs_reply_error(reply, 
            "resolved identifier '%s' is not a query", name);
        reply->code = 400;
        ecs_os_linc(&ecs_rest_query_name_error_count);
        return true;
    }

    ecs_iter_t it;
    ecs_iter_poly(world, poly->poly, &it, NULL);

    ecs_dbg_2("rest: request query '%s'", q);
    bool prev_color = ecs_log_enable_colors(false);
    rest_prev_log = ecs_os_api.log_;
    ecs_os_api.log_ = flecs_rest_capture_log;

    const char *vars = ecs_http_get_param(req, "vars");
    if (vars) {
        if (!ecs_poly_is(poly->poly, ecs_rule_t)) {
            flecs_reply_error(reply, 
                "variables are only supported for rule queries");
            reply->code = 400;
            ecs_os_linc(&ecs_rest_query_name_error_count);
            return true;
        }
        if (ecs_rule_parse_vars(poly->poly, &it, vars) == NULL) {
            flecs_rest_reply_set_captured_log(reply);
            ecs_os_linc(&ecs_rest_query_name_error_count);
            return true;
        }
    }

    flecs_rest_iter_to_reply(world, req, reply, &it);

    ecs_os_api.log_ = rest_prev_log;
    ecs_log_enable_colors(prev_color);    

    return true;
}

static
bool flecs_rest_reply_query(
    ecs_world_t *world,
    const ecs_http_request_t* req,
    ecs_http_reply_t *reply)
{
    const char *q_name = ecs_http_get_param(req, "name");
    if (q_name) {
        return flecs_rest_reply_existing_query(world, req, reply, q_name);
    }

    ecs_os_linc(&ecs_rest_query_count);

    const char *q = ecs_http_get_param(req, "q");
    if (!q) {
        ecs_strbuf_appendlit(&reply->body, "Missing parameter 'q'");
        reply->code = 400; /* bad request */
        ecs_os_linc(&ecs_rest_query_error_count);
        return true;
    }

    ecs_dbg_2("rest: request query '%s'", q);
    bool prev_color = ecs_log_enable_colors(false);
    rest_prev_log = ecs_os_api.log_;
    ecs_os_api.log_ = flecs_rest_capture_log;

    ecs_rule_t *r = ecs_rule_init(world, &(ecs_filter_desc_t){
        .expr = q
    });
    if (!r) {
        flecs_rest_reply_set_captured_log(reply);
        ecs_os_linc(&ecs_rest_query_error_count);
    } else {
        ecs_iter_t it = ecs_rule_iter(world, r);
        flecs_rest_iter_to_reply(world, req, reply, &it);
        ecs_rule_fini(r);
    }

    ecs_os_api.log_ = rest_prev_log;
    ecs_log_enable_colors(prev_color);

    return true;
}

#ifdef FLECS_MONITOR

static
void _flecs_rest_array_append(
    ecs_strbuf_t *reply,
    const char *field,
    int32_t field_len,
    const ecs_float_t *values,
    int32_t t)
{
    ecs_strbuf_list_appendch(reply, '"');
    ecs_strbuf_appendstrn(reply, field, field_len);
    ecs_strbuf_appendlit(reply, "\":");
    ecs_strbuf_list_push(reply, "[", ",");

    int32_t i;
    for (i = t + 1; i <= (t + ECS_STAT_WINDOW); i ++) {
        int32_t index = i % ECS_STAT_WINDOW;
        ecs_strbuf_list_next(reply);
        ecs_strbuf_appendflt(reply, (double)values[index], '"');
    }

    ecs_strbuf_list_pop(reply, "]");
}

#define flecs_rest_array_append(reply, field, values, t)\
    _flecs_rest_array_append(reply, field, sizeof(field) - 1, values, t)

static
void flecs_rest_gauge_append(
    ecs_strbuf_t *reply,
    const ecs_metric_t *m,
    const char *field,
    int32_t field_len,
    int32_t t,
    const char *brief,
    int32_t brief_len)
{
    ecs_strbuf_list_appendch(reply, '"');
    ecs_strbuf_appendstrn(reply, field, field_len);
    ecs_strbuf_appendlit(reply, "\":");
    ecs_strbuf_list_push(reply, "{", ",");

    flecs_rest_array_append(reply, "avg", m->gauge.avg, t);
    flecs_rest_array_append(reply, "min", m->gauge.min, t);
    flecs_rest_array_append(reply, "max", m->gauge.max, t);

    if (brief) {
        ecs_strbuf_list_appendlit(reply, "\"brief\":\"");
        ecs_strbuf_appendstrn(reply, brief, brief_len);
        ecs_strbuf_appendch(reply, '"');
    }

    ecs_strbuf_list_pop(reply, "}");
}

static
void flecs_rest_counter_append(
    ecs_strbuf_t *reply,
    const ecs_metric_t *m,
    const char *field,
    int32_t field_len,
    int32_t t,
    const char *brief,
    int32_t brief_len)
{
    flecs_rest_gauge_append(reply, m, field, field_len, t, brief, brief_len);
}

#define ECS_GAUGE_APPEND_T(reply, s, field, t, brief)\
    flecs_rest_gauge_append(reply, &(s)->field, #field, sizeof(#field) - 1, t, brief, sizeof(brief) - 1)

#define ECS_COUNTER_APPEND_T(reply, s, field, t, brief)\
    flecs_rest_counter_append(reply, &(s)->field, #field, sizeof(#field) - 1, t, brief, sizeof(brief) - 1)

#define ECS_GAUGE_APPEND(reply, s, field, brief)\
    ECS_GAUGE_APPEND_T(reply, s, field, (s)->t, brief)

#define ECS_COUNTER_APPEND(reply, s, field, brief)\
    ECS_COUNTER_APPEND_T(reply, s, field, (s)->t, brief)

static
void flecs_world_stats_to_json(
    ecs_strbuf_t *reply,
    const EcsWorldStats *monitor_stats)
{
    const ecs_world_stats_t *stats = &monitor_stats->stats;

    ecs_strbuf_list_push(reply, "{", ",");
    ECS_GAUGE_APPEND(reply, stats, entities.count, "Alive entity ids in the world");
    ECS_GAUGE_APPEND(reply, stats, entities.not_alive_count, "Not alive entity ids in the world");

    ECS_GAUGE_APPEND(reply, stats, performance.fps, "Frames per second");
    ECS_COUNTER_APPEND(reply, stats, performance.frame_time, "Time spent in frame");
    ECS_COUNTER_APPEND(reply, stats, performance.system_time, "Time spent on running systems in frame");
    ECS_COUNTER_APPEND(reply, stats, performance.emit_time, "Time spent on notifying observers in frame");
    ECS_COUNTER_APPEND(reply, stats, performance.merge_time, "Time spent on merging commands in frame");
    ECS_COUNTER_APPEND(reply, stats, performance.rematch_time, "Time spent on revalidating query caches in frame");

    ECS_COUNTER_APPEND(reply, stats, commands.add_count, "Add commands executed");
    ECS_COUNTER_APPEND(reply, stats, commands.remove_count, "Remove commands executed");
    ECS_COUNTER_APPEND(reply, stats, commands.delete_count, "Delete commands executed");
    ECS_COUNTER_APPEND(reply, stats, commands.clear_count, "Clear commands executed");
    ECS_COUNTER_APPEND(reply, stats, commands.set_count, "Set commands executed");
    ECS_COUNTER_APPEND(reply, stats, commands.get_mut_count, "Get_mut commands executed");
    ECS_COUNTER_APPEND(reply, stats, commands.modified_count, "Modified commands executed");
    ECS_COUNTER_APPEND(reply, stats, commands.other_count, "Misc commands executed");
    ECS_COUNTER_APPEND(reply, stats, commands.discard_count, "Commands for already deleted entities");
    ECS_COUNTER_APPEND(reply, stats, commands.batched_entity_count, "Entities with batched commands");
    ECS_COUNTER_APPEND(reply, stats, commands.batched_count, "Number of commands batched");

    ECS_COUNTER_APPEND(reply, stats, frame.merge_count, "Number of merges (sync points)");
    ECS_COUNTER_APPEND(reply, stats, frame.pipeline_build_count, "Pipeline rebuilds (happen when systems become active/enabled)");
    ECS_COUNTER_APPEND(reply, stats, frame.systems_ran, "Systems ran in frame");
    ECS_COUNTER_APPEND(reply, stats, frame.observers_ran, "Number of times an observer was invoked in frame");
    ECS_COUNTER_APPEND(reply, stats, frame.event_emit_count, "Events emitted in frame");
    ECS_COUNTER_APPEND(reply, stats, frame.rematch_count, "Number of query cache revalidations");

    ECS_GAUGE_APPEND(reply, stats, tables.count, "Tables in the world (including empty)");
    ECS_GAUGE_APPEND(reply, stats, tables.empty_count, "Empty tables in the world");
    ECS_GAUGE_APPEND(reply, stats, tables.tag_only_count, "Tables with only tags");
    ECS_GAUGE_APPEND(reply, stats, tables.trivial_only_count, "Tables with only trivial types (no hooks)");
    ECS_GAUGE_APPEND(reply, stats, tables.record_count, "Table records registered with search indices");
    ECS_GAUGE_APPEND(reply, stats, tables.storage_count, "Component storages for all tables");
    ECS_COUNTER_APPEND(reply, stats, tables.create_count, "Number of new tables created");
    ECS_COUNTER_APPEND(reply, stats, tables.delete_count, "Number of tables deleted");

    ECS_GAUGE_APPEND(reply, stats, ids.count, "Component, tag and pair ids in use");
    ECS_GAUGE_APPEND(reply, stats, ids.tag_count, "Tag ids in use");
    ECS_GAUGE_APPEND(reply, stats, ids.component_count, "Component ids in use");
    ECS_GAUGE_APPEND(reply, stats, ids.pair_count, "Pair ids in use");
    ECS_GAUGE_APPEND(reply, stats, ids.wildcard_count, "Wildcard ids in use");
    ECS_GAUGE_APPEND(reply, stats, ids.type_count, "Registered component types");
    ECS_COUNTER_APPEND(reply, stats, ids.create_count, "Number of new component, tag and pair ids created");
    ECS_COUNTER_APPEND(reply, stats, ids.delete_count, "Number of component, pair and tag ids deleted");

    ECS_GAUGE_APPEND(reply, stats, queries.query_count, "Queries in the world");
    ECS_GAUGE_APPEND(reply, stats, queries.observer_count, "Observers in the world");
    ECS_GAUGE_APPEND(reply, stats, queries.system_count, "Systems in the world");

    ECS_COUNTER_APPEND(reply, stats, memory.alloc_count, "Allocations by OS API");
    ECS_COUNTER_APPEND(reply, stats, memory.realloc_count, "Reallocs by OS API");
    ECS_COUNTER_APPEND(reply, stats, memory.free_count, "Frees by OS API");
    ECS_GAUGE_APPEND(reply, stats, memory.outstanding_alloc_count, "Outstanding allocations by OS API");
    ECS_COUNTER_APPEND(reply, stats, memory.block_alloc_count, "Blocks allocated by block allocators");
    ECS_COUNTER_APPEND(reply, stats, memory.block_free_count, "Blocks freed by block allocators");
    ECS_GAUGE_APPEND(reply, stats, memory.block_outstanding_alloc_count, "Outstanding block allocations");
    ECS_COUNTER_APPEND(reply, stats, memory.stack_alloc_count, "Pages allocated by stack allocators");
    ECS_COUNTER_APPEND(reply, stats, memory.stack_free_count, "Pages freed by stack allocators");
    ECS_GAUGE_APPEND(reply, stats, memory.stack_outstanding_alloc_count, "Outstanding page allocations");

    ECS_COUNTER_APPEND(reply, stats, rest.request_count, "Received requests");
    ECS_COUNTER_APPEND(reply, stats, rest.entity_count, "Received entity/ requests");
    ECS_COUNTER_APPEND(reply, stats, rest.entity_error_count, "Failed entity/ requests");
    ECS_COUNTER_APPEND(reply, stats, rest.query_count, "Received query/ requests");
    ECS_COUNTER_APPEND(reply, stats, rest.query_error_count, "Failed query/ requests");
    ECS_COUNTER_APPEND(reply, stats, rest.query_name_count, "Received named query/ requests");
    ECS_COUNTER_APPEND(reply, stats, rest.query_name_error_count, "Failed named query/ requests");
    ECS_COUNTER_APPEND(reply, stats, rest.query_name_from_cache_count, "Named query/ requests from cache");
    ECS_COUNTER_APPEND(reply, stats, rest.enable_count, "Received enable/ and disable/ requests");
    ECS_COUNTER_APPEND(reply, stats, rest.enable_error_count, "Failed enable/ and disable/ requests");
    ECS_COUNTER_APPEND(reply, stats, rest.world_stats_count, "Received world stats requests");
    ECS_COUNTER_APPEND(reply, stats, rest.pipeline_stats_count, "Received pipeline stats requests");
    ECS_COUNTER_APPEND(reply, stats, rest.stats_error_count, "Failed stats requests");

    ECS_COUNTER_APPEND(reply, stats, http.request_received_count, "Received requests");
    ECS_COUNTER_APPEND(reply, stats, http.request_invalid_count, "Received invalid requests");
    ECS_COUNTER_APPEND(reply, stats, http.request_handled_ok_count, "Requests handled successfully");
    ECS_COUNTER_APPEND(reply, stats, http.request_handled_error_count, "Requests handled with error code");
    ECS_COUNTER_APPEND(reply, stats, http.request_not_handled_count, "Requests not handled (unknown endpoint)");
    ECS_COUNTER_APPEND(reply, stats, http.request_preflight_count, "Preflight requests received");
    ECS_COUNTER_APPEND(reply, stats, http.send_ok_count, "Successful replies");
    ECS_COUNTER_APPEND(reply, stats, http.send_error_count, "Unsuccessful replies");
    ECS_COUNTER_APPEND(reply, stats, http.busy_count, "Dropped requests due to full send queue (503)");

    ecs_strbuf_list_pop(reply, "}");
}

static
void flecs_system_stats_to_json(
    ecs_world_t *world,
    ecs_strbuf_t *reply,
    ecs_entity_t system,
    const ecs_system_stats_t *stats)
{
    ecs_strbuf_list_push(reply, "{", ",");
    ecs_strbuf_list_appendlit(reply, "\"name\":\"");
    ecs_get_path_w_sep_buf(world, 0, system, ".", NULL, reply);
    ecs_strbuf_appendch(reply, '"');

    if (!stats->task) {
        ECS_GAUGE_APPEND(reply, &stats->query, matched_table_count, "");
        ECS_GAUGE_APPEND(reply, &stats->query, matched_entity_count, "");
    }

    ECS_COUNTER_APPEND_T(reply, stats, time_spent, stats->query.t, "");
    ecs_strbuf_list_pop(reply, "}");
}

static
void flecs_pipeline_stats_to_json(
    ecs_world_t *world,
    ecs_strbuf_t *reply,
    const EcsPipelineStats *stats)
{
    ecs_strbuf_list_push(reply, "[", ",");

    int32_t i, count = ecs_vec_count(&stats->stats.systems);
    ecs_entity_t *ids = ecs_vec_first_t(&stats->stats.systems, ecs_entity_t);
    for (i = 0; i < count; i ++) {
        ecs_entity_t id = ids[i];
        
        ecs_strbuf_list_next(reply);

        if (id) {
            ecs_system_stats_t *sys_stats = ecs_map_get_deref(
                &stats->stats.system_stats, ecs_system_stats_t, id);
            flecs_system_stats_to_json(world, reply, id, sys_stats);
        } else {
            /* Sync point */
            ecs_strbuf_list_push(reply, "{", ",");
            ecs_strbuf_list_pop(reply, "}");
        }
    }

    ecs_strbuf_list_pop(reply, "]");
}

static
bool flecs_rest_reply_stats(
    ecs_world_t *world,
    const ecs_http_request_t* req,
    ecs_http_reply_t *reply)
{
    char *period_str = NULL;
    flecs_rest_string_param(req, "period", &period_str);
    char *category = &req->path[6];

    ecs_entity_t period = EcsPeriod1s;
    if (period_str) {
        char *period_name = ecs_asprintf("Period%s", period_str);
        period = ecs_lookup_child(world, ecs_id(FlecsMonitor), period_name);
        ecs_os_free(period_name);
        if (!period) {
            flecs_reply_error(reply, "bad request (invalid period string)");
            reply->code = 400;
            ecs_os_linc(&ecs_rest_stats_error_count);
            return false;
        }
    }

    if (!ecs_os_strcmp(category, "world")) {
        const EcsWorldStats *stats = ecs_get_pair(world, EcsWorld, 
            EcsWorldStats, period);
        flecs_world_stats_to_json(&reply->body, stats);
        ecs_os_linc(&ecs_rest_world_stats_count);
        return true;

    } else if (!ecs_os_strcmp(category, "pipeline")) {
        const EcsPipelineStats *stats = ecs_get_pair(world, EcsWorld, 
            EcsPipelineStats, period);
        flecs_pipeline_stats_to_json(world, &reply->body, stats);
        ecs_os_linc(&ecs_rest_pipeline_stats_count);
        return true;

    } else {
        flecs_reply_error(reply, "bad request (unsupported category)");
        reply->code = 400;
        ecs_os_linc(&ecs_rest_stats_error_count);
        return false;
    }

    return true;
}
#else
static
bool flecs_rest_reply_stats(
    ecs_world_t *world,
    const ecs_http_request_t* req,
    ecs_http_reply_t *reply)
{
    (void)world;
    (void)req;
    (void)reply;
    return false;
}
#endif

static
void flecs_rest_reply_table_append_type(
    ecs_world_t *world,
    ecs_strbuf_t *reply,
    const ecs_table_t *table)
{
    ecs_strbuf_list_push(reply, "[", ",");
    int32_t i, count = table->type.count;
    ecs_id_t *ids = table->type.array;
    for (i = 0; i < count; i ++) {
        ecs_strbuf_list_next(reply);
        ecs_strbuf_appendch(reply, '"');
        ecs_id_str_buf(world, ids[i], reply);
        ecs_strbuf_appendch(reply, '"');
    }
    ecs_strbuf_list_pop(reply, "]");
}

static
void flecs_rest_reply_table_append_memory(
    ecs_strbuf_t *reply,
    const ecs_table_t *table)
{
    int32_t used = 0, allocated = 0;

    used += table->data.entities.count * ECS_SIZEOF(ecs_entity_t);
    used += table->data.records.count * ECS_SIZEOF(ecs_record_t*);
    allocated += table->data.entities.size * ECS_SIZEOF(ecs_entity_t);
    allocated += table->data.records.size * ECS_SIZEOF(ecs_record_t*);

    int32_t i, storage_count = table->storage_count;
    ecs_type_info_t **ti = table->type_info;
    ecs_vec_t *storages = table->data.columns;

    for (i = 0; i < storage_count; i ++) {
        used += storages[i].count * ti[i]->size;
        allocated += storages[i].size * ti[i]->size;
    }

    ecs_strbuf_list_push(reply, "{", ",");
    ecs_strbuf_list_append(reply, "\"used\":%d", used);
    ecs_strbuf_list_append(reply, "\"allocated\":%d", allocated);
    ecs_strbuf_list_pop(reply, "}");
}

static
void flecs_rest_reply_table_append(
    ecs_world_t *world,
    ecs_strbuf_t *reply,
    const ecs_table_t *table)
{
    ecs_strbuf_list_next(reply);
    ecs_strbuf_list_push(reply, "{", ",");
    ecs_strbuf_list_append(reply, "\"id\":%u", (uint32_t)table->id);
    ecs_strbuf_list_appendstr(reply, "\"type\":");
    flecs_rest_reply_table_append_type(world, reply, table);
    ecs_strbuf_list_append(reply, "\"count\":%d", ecs_table_count(table));
    ecs_strbuf_list_append(reply, "\"memory\":");
    flecs_rest_reply_table_append_memory(reply, table);
    ecs_strbuf_list_append(reply, "\"refcount\":%d", table->_->refcount);
    ecs_strbuf_list_pop(reply, "}");
}

static
bool flecs_rest_reply_tables(
    ecs_world_t *world,
    const ecs_http_request_t* req,
    ecs_http_reply_t *reply)
{
    (void)req;

    ecs_strbuf_list_push(&reply->body, "[", ",");
    ecs_sparse_t *tables = &world->store.tables;
    int32_t i, count = flecs_sparse_count(tables);
    for (i = 0; i < count; i ++) {
        ecs_table_t *table = flecs_sparse_get_dense_t(tables, ecs_table_t, i);
        flecs_rest_reply_table_append(world, &reply->body, table);
    }
    ecs_strbuf_list_pop(&reply->body, "]");

    return true;
}

static
bool flecs_rest_reply(
    const ecs_http_request_t* req,
    ecs_http_reply_t *reply,
    void *ctx)
{
    ecs_rest_ctx_t *impl = ctx;
    ecs_world_t *world = impl->world;

    ecs_os_linc(&ecs_rest_request_count);

    if (req->path == NULL) {
        ecs_dbg("rest: bad request (missing path)");
        flecs_reply_error(reply, "bad request (missing path)");
        reply->code = 400;
        return false;
    }

    if (req->method == EcsHttpGet) {
        /* Entity endpoint */
        if (!ecs_os_strncmp(req->path, "entity/", 7)) {
            return flecs_rest_reply_entity(world, req, reply);

        /* Query endpoint */
        } else if (!ecs_os_strcmp(req->path, "query")) {
            return flecs_rest_reply_query(world, req, reply);

        /* World endpoint */
        } else if (!ecs_os_strcmp(req->path, "world")) {
            return flecs_rest_reply_world(world, req, reply);

        /* Stats endpoint */
        } else if (!ecs_os_strncmp(req->path, "stats/", 6)) {
            return flecs_rest_reply_stats(world, req, reply);

        /* Tables endpoint */
        } else if (!ecs_os_strncmp(req->path, "tables", 6)) {
            return flecs_rest_reply_tables(world, req, reply);
        }

    } else if (req->method == EcsHttpPut) {
        /* Set endpoint */
        if (!ecs_os_strncmp(req->path, "set/", 4)) {
            return flecs_rest_set(world, req, reply, &req->path[4]);
        
        /* Delete endpoint */
        } else if (!ecs_os_strncmp(req->path, "delete/", 7)) {
            return flecs_rest_delete(world, reply, &req->path[7]);

        /* Enable endpoint */
        } else if (!ecs_os_strncmp(req->path, "enable/", 7)) {
            return flecs_rest_enable(world, reply, &req->path[7], true);

        /* Disable endpoint */
        } else if (!ecs_os_strncmp(req->path, "disable/", 8)) {
            return flecs_rest_enable(world, reply, &req->path[8], false);

        /* Script endpoint */
        } else if (!ecs_os_strncmp(req->path, "script", 6)) {
            return flecs_rest_script(world, req, reply);
        }
    }

    return false;
}

ecs_http_server_t* ecs_rest_server_init(
    ecs_world_t *world,
    const ecs_http_server_desc_t *desc)
{
    ecs_rest_ctx_t *srv_ctx = ecs_os_calloc_t(ecs_rest_ctx_t);
    ecs_http_server_desc_t private_desc = {0};
    if (desc) {
        private_desc = *desc;
    }
    private_desc.callback = flecs_rest_reply;
    private_desc.ctx = srv_ctx;

    ecs_http_server_t *srv = ecs_http_server_init(&private_desc);
    if (!srv) {
        ecs_os_free(srv_ctx);
        return NULL;
    }

    srv_ctx->world = world;
    srv_ctx->srv = srv;
    srv_ctx->rc = 1;
    srv_ctx->srv = srv;
    return srv;
}

void ecs_rest_server_fini(
    ecs_http_server_t *srv)
{
    ecs_rest_ctx_t *srv_ctx = ecs_http_server_ctx(srv);
    ecs_os_free(srv_ctx);
    ecs_http_server_fini(srv);
}

static
void flecs_on_set_rest(ecs_iter_t *it) {
    EcsRest *rest = it->ptrs[0];

    int i;
    for(i = 0; i < it->count; i ++) {
        if (!rest[i].port) {
            rest[i].port = ECS_REST_DEFAULT_PORT;
        }

        ecs_http_server_t *srv = ecs_rest_server_init(it->real_world,
            &(ecs_http_server_desc_t){ 
                .ipaddr = rest[i].ipaddr, 
                .port = rest[i].port 
            });

        if (!srv) {
            const char *ipaddr = rest[i].ipaddr ? rest[i].ipaddr : "0.0.0.0";
            ecs_err("failed to create REST server on %s:%u", 
                ipaddr, rest[i].port);
            continue;
        }

        rest[i].impl = ecs_http_server_ctx(srv);

        ecs_http_server_start(srv);
    }
}

static
void DequeueRest(ecs_iter_t *it) {
    EcsRest *rest = ecs_field(it, EcsRest, 1);

    if (it->delta_system_time > (ecs_ftime_t)1.0) {
        ecs_warn(
            "detected large progress interval (%.2fs), REST request may timeout",
            (double)it->delta_system_time);
    }

    int32_t i;
    for(i = 0; i < it->count; i ++) {
        ecs_rest_ctx_t *ctx = rest[i].impl;
        if (ctx) {
            ecs_http_server_dequeue(ctx->srv, it->delta_time);
        }
    } 
}

static
void DisableRest(ecs_iter_t *it) {
    ecs_world_t *world = it->world;

    ecs_iter_t rit = ecs_term_iter(world, &(ecs_term_t){
        .id = ecs_id(EcsRest),
        .src.flags = EcsSelf
    });

    if (it->event == EcsOnAdd) {
        /* REST module was disabled */
        while (ecs_term_next(&rit)) {
            EcsRest *rest = ecs_field(&rit, EcsRest, 1);
            int i;
            for (i = 0; i < rit.count; i ++) {
                ecs_rest_ctx_t *ctx = rest[i].impl;
                ecs_http_server_stop(ctx->srv);
            }
        }
    } else if (it->event == EcsOnRemove) {
        /* REST module was enabled */
        while (ecs_term_next(&rit)) {
            EcsRest *rest = ecs_field(&rit, EcsRest, 1);
            int i;
            for (i = 0; i < rit.count; i ++) {
                ecs_rest_ctx_t *ctx = rest[i].impl;
                ecs_http_server_start(ctx->srv);
            }
        }
    }
}

void FlecsRestImport(
    ecs_world_t *world)
{
    ECS_MODULE(world, FlecsRest);

    ECS_IMPORT(world, FlecsPipeline);
#ifdef FLECS_PLECS
    ECS_IMPORT(world, FlecsScript);
#endif

    ecs_set_name_prefix(world, "Ecs");

    flecs_bootstrap_component(world, EcsRest);

    ecs_set_hooks(world, EcsRest, { 
        .ctor = ecs_default_ctor,
        .move = ecs_move(EcsRest),
        .copy = ecs_copy(EcsRest),
        .dtor = ecs_dtor(EcsRest),
        .on_set = flecs_on_set_rest
    });

    ECS_SYSTEM(world, DequeueRest, EcsPostFrame, EcsRest);

    ecs_system(world, {
        .entity = ecs_id(DequeueRest),
        .no_readonly = true
    });

    ecs_observer(world, {
        .filter = { 
            .terms = {{ .id = EcsDisabled, .src.id = ecs_id(FlecsRest) }}
        },
        .events = {EcsOnAdd, EcsOnRemove},
        .callback = DisableRest
    });

    ecs_set_name_prefix(world, "EcsRest");
    ECS_TAG_DEFINE(world, EcsRestPlecs);
}

#endif

/**
 * @file addons/coredoc.c
 * @brief Core doc addon.
 */


#ifdef FLECS_COREDOC

#define URL_ROOT "https://www.flecs.dev/flecs/md_docs_Relationships.html/"

void FlecsCoreDocImport(
    ecs_world_t *world)
{    
    ECS_MODULE(world, FlecsCoreDoc);

    ECS_IMPORT(world, FlecsMeta);
    ECS_IMPORT(world, FlecsDoc);

    ecs_set_name_prefix(world, "Ecs");

    /* Initialize reflection data for core components */

    ecs_struct_init(world, &(ecs_struct_desc_t){
        .entity = ecs_id(EcsComponent),
        .members = {
            {.name = (char*)"size", .type = ecs_id(ecs_i32_t)},
            {.name = (char*)"alignment", .type = ecs_id(ecs_i32_t)}
        }
    });

    ecs_struct_init(world, &(ecs_struct_desc_t){
        .entity = ecs_id(EcsDocDescription),
        .members = {
            {.name = "value", .type = ecs_id(ecs_string_t)}
        }
    });

    /* Initialize documentation data for core components */
    ecs_doc_set_brief(world, EcsFlecs, "Flecs root module");
    ecs_doc_set_link(world, EcsFlecs, "https://github.com/SanderMertens/flecs");

    ecs_doc_set_brief(world, EcsFlecsCore, "Flecs module with builtin components");

    ecs_doc_set_brief(world, EcsWorld, "Entity associated with world");

    ecs_doc_set_brief(world, ecs_id(EcsComponent), "Component that is added to all components");
    ecs_doc_set_brief(world, EcsModule, "Tag that is added to modules");
    ecs_doc_set_brief(world, EcsPrefab, "Tag that is added to prefabs");
    ecs_doc_set_brief(world, EcsDisabled, "Tag that is added to disabled entities");

    ecs_doc_set_brief(world, ecs_id(EcsIdentifier), "Component used for entity names");
    ecs_doc_set_brief(world, EcsName, "Tag used with EcsIdentifier to signal entity name");
    ecs_doc_set_brief(world, EcsSymbol, "Tag used with EcsIdentifier to signal entity symbol");

    ecs_doc_set_brief(world, EcsTransitive, "Transitive relationship property");
    ecs_doc_set_brief(world, EcsReflexive, "Reflexive relationship property");
    ecs_doc_set_brief(world, EcsFinal, "Final relationship property");
    ecs_doc_set_brief(world, EcsDontInherit, "DontInherit relationship property");
    ecs_doc_set_brief(world, EcsTag, "Tag relationship property");
    ecs_doc_set_brief(world, EcsAcyclic, "Acyclic relationship property");
    ecs_doc_set_brief(world, EcsTraversable, "Traversable relationship property");
    ecs_doc_set_brief(world, EcsExclusive, "Exclusive relationship property");
    ecs_doc_set_brief(world, EcsSymmetric, "Symmetric relationship property");
    ecs_doc_set_brief(world, EcsWith, "With relationship property");
    ecs_doc_set_brief(world, EcsOnDelete, "OnDelete relationship cleanup property");
    ecs_doc_set_brief(world, EcsOnDeleteTarget, "OnDeleteTarget relationship cleanup property");
    ecs_doc_set_brief(world, EcsDefaultChildComponent, "Sets default component hint for children of entity");
    ecs_doc_set_brief(world, EcsRemove, "Remove relationship cleanup property");
    ecs_doc_set_brief(world, EcsDelete, "Delete relationship cleanup property");
    ecs_doc_set_brief(world, EcsPanic, "Panic relationship cleanup property");
    ecs_doc_set_brief(world, EcsIsA, "Builtin IsA relationship");
    ecs_doc_set_brief(world, EcsChildOf, "Builtin ChildOf relationship");
    ecs_doc_set_brief(world, EcsDependsOn, "Builtin DependsOn relationship");
    ecs_doc_set_brief(world, EcsOnAdd, "Builtin OnAdd event");
    ecs_doc_set_brief(world, EcsOnRemove, "Builtin OnRemove event");
    ecs_doc_set_brief(world, EcsOnSet, "Builtin OnSet event");
    ecs_doc_set_brief(world, EcsUnSet, "Builtin UnSet event");

    ecs_doc_set_link(world, EcsTransitive, URL_ROOT "#transitive-property");
    ecs_doc_set_link(world, EcsReflexive, URL_ROOT "#reflexive-property");
    ecs_doc_set_link(world, EcsFinal, URL_ROOT "#final-property");
    ecs_doc_set_link(world, EcsDontInherit, URL_ROOT "#dontinherit-property");
    ecs_doc_set_link(world, EcsTag, URL_ROOT "#tag-property");
    ecs_doc_set_link(world, EcsAcyclic, URL_ROOT "#acyclic-property");
    ecs_doc_set_link(world, EcsTraversable, URL_ROOT "#traversable-property");
    ecs_doc_set_link(world, EcsExclusive, URL_ROOT "#exclusive-property");
    ecs_doc_set_link(world, EcsSymmetric, URL_ROOT "#symmetric-property");
    ecs_doc_set_link(world, EcsWith, URL_ROOT "#with-property");
    ecs_doc_set_link(world, EcsOnDelete, URL_ROOT "#cleanup-properties");
    ecs_doc_set_link(world, EcsOnDeleteTarget, URL_ROOT "#cleanup-properties");
    ecs_doc_set_link(world, EcsRemove, URL_ROOT "#cleanup-properties");
    ecs_doc_set_link(world, EcsDelete, URL_ROOT "#cleanup-properties");
    ecs_doc_set_link(world, EcsPanic, URL_ROOT "#cleanup-properties");
    ecs_doc_set_link(world, EcsIsA, URL_ROOT "#the-isa-relationship");
    ecs_doc_set_link(world, EcsChildOf, URL_ROOT "#the-childof-relationship"); 
    
    /* Initialize documentation for meta components */
    ecs_entity_t meta = ecs_lookup_fullpath(world, "flecs.meta");
    ecs_doc_set_brief(world, meta, "Flecs module with reflection components");

    ecs_doc_set_brief(world, ecs_id(EcsMetaType), "Component added to types");
    ecs_doc_set_brief(world, ecs_id(EcsMetaTypeSerialized), "Component that stores reflection data in an optimized format");
    ecs_doc_set_brief(world, ecs_id(EcsPrimitive), "Component added to primitive types");
    ecs_doc_set_brief(world, ecs_id(EcsEnum), "Component added to enumeration types");
    ecs_doc_set_brief(world, ecs_id(EcsBitmask), "Component added to bitmask types");
    ecs_doc_set_brief(world, ecs_id(EcsMember), "Component added to struct members");
    ecs_doc_set_brief(world, ecs_id(EcsStruct), "Component added to struct types");
    ecs_doc_set_brief(world, ecs_id(EcsArray), "Component added to array types");
    ecs_doc_set_brief(world, ecs_id(EcsVector), "Component added to vector types");

    ecs_doc_set_brief(world, ecs_id(ecs_bool_t), "bool component");
    ecs_doc_set_brief(world, ecs_id(ecs_char_t), "char component");
    ecs_doc_set_brief(world, ecs_id(ecs_byte_t), "byte component");
    ecs_doc_set_brief(world, ecs_id(ecs_u8_t), "8 bit unsigned int component");
    ecs_doc_set_brief(world, ecs_id(ecs_u16_t), "16 bit unsigned int component");
    ecs_doc_set_brief(world, ecs_id(ecs_u32_t), "32 bit unsigned int component");
    ecs_doc_set_brief(world, ecs_id(ecs_u64_t), "64 bit unsigned int component");
    ecs_doc_set_brief(world, ecs_id(ecs_uptr_t), "word sized unsigned int component");
    ecs_doc_set_brief(world, ecs_id(ecs_i8_t), "8 bit signed int component");
    ecs_doc_set_brief(world, ecs_id(ecs_i16_t), "16 bit signed int component");
    ecs_doc_set_brief(world, ecs_id(ecs_i32_t), "32 bit signed int component");
    ecs_doc_set_brief(world, ecs_id(ecs_i64_t), "64 bit signed int component");
    ecs_doc_set_brief(world, ecs_id(ecs_iptr_t), "word sized signed int component");
    ecs_doc_set_brief(world, ecs_id(ecs_f32_t), "32 bit floating point component");
    ecs_doc_set_brief(world, ecs_id(ecs_f64_t), "64 bit floating point component");
    ecs_doc_set_brief(world, ecs_id(ecs_string_t), "string component");
    ecs_doc_set_brief(world, ecs_id(ecs_entity_t), "entity component");

    /* Initialize documentation for doc components */
    ecs_entity_t doc = ecs_lookup_fullpath(world, "flecs.doc");
    ecs_doc_set_brief(world, doc, "Flecs module with documentation components");

    ecs_doc_set_brief(world, ecs_id(EcsDocDescription), "Component used to add documentation");
    ecs_doc_set_brief(world, EcsDocBrief, "Used as (Description, Brief) to add a brief description");
    ecs_doc_set_brief(world, EcsDocDetail, "Used as (Description, Detail) to add a detailed description");
    ecs_doc_set_brief(world, EcsDocLink, "Used as (Description, Link) to add a link");
}

#endif

/**
 * @file addons/http.c
 * @brief HTTP addon.
 *
 * This is a heavily modified version of the EmbeddableWebServer (see copyright
 * below). This version has been stripped from everything not strictly necessary
 * for receiving/replying to simple HTTP requests, and has been modified to use
 * the Flecs OS API.
 *
 * EmbeddableWebServer Copyright (c) 2016, 2019, 2020 Forrest Heller, and 
 * CONTRIBUTORS (see below) - All rights reserved.
 *
 * CONTRIBUTORS:
 * Martin Pulec - bug fixes, warning fixes, IPv6 support
 * Daniel Barry - bug fix (ifa_addr != NULL)
 * 
 * Released under the BSD 2-clause license:
 * Redistribution and use in source and binary forms, with or without 
 * modification, are permitted provided that the following conditions are met:
 * 1. Redistributions of source code must retain the above copyright notice, 
 * this list of conditions and the following disclaimer.
 * 2. Redistributions in binary form must reproduce the above copyright notice, 
 * this list of conditions and the following disclaimer in the documentation 
 * and/or other materials provided with the distribution. THIS SOFTWARE IS 
 * PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS 
 * OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES 
 * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN 
 * NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY 
 * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 
 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 
 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 
 * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF 
 * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 
 */


#ifdef FLECS_HTTP

#if defined(ECS_TARGET_WINDOWS)
#ifndef WIN32_LEAN_AND_MEAN
#define WIN32_LEAN_AND_MEAN
#endif
#pragma comment(lib, "Ws2_32.lib")
#include <winsock2.h>
#include <ws2tcpip.h>
#include <windows.h>
typedef SOCKET ecs_http_socket_t;
#else
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <sys/time.h>
#include <netdb.h>
#include <strings.h>
#include <signal.h>
#include <fcntl.h>
typedef int ecs_http_socket_t;
#endif

#if !defined(MSG_NOSIGNAL)
#define MSG_NOSIGNAL (0)
#endif

/* Max length of request method */
#define ECS_HTTP_METHOD_LEN_MAX (8) 

/* Timeout (s) before connection purge */
#define ECS_HTTP_CONNECTION_PURGE_TIMEOUT (1.0)

/* Number of dequeues before purging */
#define ECS_HTTP_CONNECTION_PURGE_RETRY_COUNT (5)

/* Number of retries receiving request */
#define ECS_HTTP_REQUEST_RECV_RETRY (10)

/* Minimum interval between dequeueing requests (ms) */
#define ECS_HTTP_MIN_DEQUEUE_INTERVAL (50)

/* Minimum interval between printing statistics (ms) */
#define ECS_HTTP_MIN_STATS_INTERVAL (10 * 1000)

/* Max length of headers in reply */
#define ECS_HTTP_REPLY_HEADER_SIZE (1024)

/* Receive buffer size */
#define ECS_HTTP_SEND_RECV_BUFFER_SIZE (16 * 1024)

/* Max length of request (path + query + headers + body) */
#define ECS_HTTP_REQUEST_LEN_MAX (10 * 1024 * 1024)

/* Total number of outstanding send requests */
#define ECS_HTTP_SEND_QUEUE_MAX (256)

/* Cache invalidation timeout (s) */
#define ECS_HTTP_CACHE_TIMEOUT ((ecs_ftime_t)1.0)

/* Cache entry purge timeout (s) */
#define ECS_HTTP_CACHE_PURGE_TIMEOUT ((ecs_ftime_t)10.0)

/* Global statistics */
int64_t ecs_http_request_received_count = 0;
int64_t ecs_http_request_invalid_count = 0;
int64_t ecs_http_request_handled_ok_count = 0;
int64_t ecs_http_request_handled_error_count = 0;
int64_t ecs_http_request_not_handled_count = 0;
int64_t ecs_http_request_preflight_count = 0;
int64_t ecs_http_send_ok_count = 0;
int64_t ecs_http_send_error_count = 0;
int64_t ecs_http_busy_count = 0;

/* Send request queue */
typedef struct ecs_http_send_request_t {
    ecs_http_socket_t sock;
    char *headers;
    int32_t header_length;
    char *content;
    int32_t content_length;
} ecs_http_send_request_t;

typedef struct ecs_http_send_queue_t {
    ecs_http_send_request_t requests[ECS_HTTP_SEND_QUEUE_MAX];
    int32_t head;
    int32_t tail;
    ecs_os_thread_t thread;
    int32_t wait_ms;
} ecs_http_send_queue_t;

typedef struct ecs_http_request_key_t {
    const char *array;
    ecs_size_t count;
} ecs_http_request_key_t;

typedef struct ecs_http_request_entry_t {
    char *content;
    int32_t content_length;
    ecs_ftime_t time;
} ecs_http_request_entry_t;

/* HTTP server struct */
struct ecs_http_server_t {
    bool should_run;
    bool running;

    ecs_http_socket_t sock;
    ecs_os_mutex_t lock;
    ecs_os_thread_t thread;

    ecs_http_reply_action_t callback;
    void *ctx;

    ecs_sparse_t connections; /* sparse<http_connection_t> */
    ecs_sparse_t requests; /* sparse<http_request_t> */

    bool initialized;

    uint16_t port;
    const char *ipaddr;

    double dequeue_timeout; /* used to not lock request queue too often */
    double stats_timeout; /* used for periodic reporting of statistics */

    double request_time; /* time spent on requests in last stats interval */
    double request_time_total; /* total time spent on requests */
    int32_t requests_processed; /* requests processed in last stats interval */
    int32_t requests_processed_total; /* total requests processed */
    int32_t dequeue_count; /* number of dequeues in last stats interval */ 
    ecs_http_send_queue_t send_queue;

    ecs_hashmap_t request_cache;
};

/** Fragment state, used by HTTP request parser */
typedef enum  {
    HttpFragStateBegin,
    HttpFragStateMethod,
    HttpFragStatePath,
    HttpFragStateVersion,
    HttpFragStateHeaderStart,
    HttpFragStateHeaderName,
    HttpFragStateHeaderValueStart,
    HttpFragStateHeaderValue,
    HttpFragStateCR,
    HttpFragStateCRLF,
    HttpFragStateCRLFCR,
    HttpFragStateBody,
    HttpFragStateDone
} HttpFragState;

/** A fragment is a partially received HTTP request */
typedef struct {
    HttpFragState state;
    ecs_strbuf_t buf;
    ecs_http_method_t method;
    int32_t body_offset;
    int32_t query_offset;
    int32_t header_offsets[ECS_HTTP_HEADER_COUNT_MAX];
    int32_t header_value_offsets[ECS_HTTP_HEADER_COUNT_MAX];
    int32_t header_count;
    int32_t param_offsets[ECS_HTTP_QUERY_PARAM_COUNT_MAX];
    int32_t param_value_offsets[ECS_HTTP_QUERY_PARAM_COUNT_MAX];
    int32_t param_count;
    int32_t content_length;
    char *header_buf_ptr;
    char header_buf[32];
    bool parse_content_length;
    bool invalid;
} ecs_http_fragment_t;

/** Extend public connection type with fragment data */
typedef struct {
    ecs_http_connection_t pub;
    ecs_http_socket_t sock;

    /* Connection is purged after both timeout expires and connection has
     * exceeded retry count. This ensures that a connection does not immediately
     * timeout when a frame takes longer than usual */
    double dequeue_timeout;
    int32_t dequeue_retries;    
} ecs_http_connection_impl_t;

typedef struct {
    ecs_http_request_t pub;
    uint64_t conn_id; /* for sanity check */
    char *res;
    int32_t req_len;
} ecs_http_request_impl_t;

static
ecs_size_t http_send(
    ecs_http_socket_t sock, 
    const void *buf, 
    ecs_size_t size, 
    int flags)
{
    ecs_assert(size >= 0, ECS_INTERNAL_ERROR, NULL);
#ifdef ECS_TARGET_POSIX
    ssize_t send_bytes = send(sock, buf, flecs_itosize(size), 
        flags | MSG_NOSIGNAL);
    return flecs_itoi32(send_bytes);
#else
    int send_bytes = send(sock, buf, size, flags);
    return flecs_itoi32(send_bytes);
#endif
}

static
ecs_size_t http_recv(
    ecs_http_socket_t sock,
    void *buf,
    ecs_size_t size,
    int flags)
{
    ecs_size_t ret;
#ifdef ECS_TARGET_POSIX
    ssize_t recv_bytes = recv(sock, buf, flecs_itosize(size), flags);
    ret = flecs_itoi32(recv_bytes);
#else
    int recv_bytes = recv(sock, buf, size, flags);
    ret = flecs_itoi32(recv_bytes);
#endif
    if (ret == -1) {
        ecs_dbg("recv failed: %s (sock = %d)", ecs_os_strerror(errno), sock);
    } else if (ret == 0) {
        ecs_dbg("recv: received 0 bytes (sock = %d)", sock);
    }

    return ret;
}

static
void http_sock_set_timeout(
    ecs_http_socket_t sock,
    int32_t timeout_ms)
{
    int r;
#ifdef ECS_TARGET_POSIX
    struct timeval tv;
    tv.tv_sec = timeout_ms * 1000;
    tv.tv_usec = 0;
    r = setsockopt(sock, SOL_SOCKET, SO_RCVTIMEO, (const char*)&tv, sizeof tv);
#else
    DWORD t = (DWORD)timeout_ms;
    r = setsockopt(sock, SOL_SOCKET, SO_RCVTIMEO, (const char*)&t, sizeof t);
#endif
    if (r) {
        ecs_warn("http: failed to set socket timeout: %s", 
            ecs_os_strerror(errno));
    }
}

static
void http_sock_keep_alive(
    ecs_http_socket_t sock)
{
    int v = 1;
    if (setsockopt(sock, SOL_SOCKET, SO_KEEPALIVE, (const char*)&v, sizeof v)) {
        ecs_warn("http: failed to set socket KEEPALIVE: %s",
            ecs_os_strerror(errno));
    }
}

static
void http_sock_nonblock(ecs_http_socket_t sock, bool enable) {
    (void)sock;
#ifdef ECS_TARGET_POSIX
    int flags;
    flags = fcntl(sock,F_GETFL,0);
    if (flags == -1) {
        ecs_warn("http: failed to set socket NONBLOCK: %s",
            ecs_os_strerror(errno));
        return;
    }
    if (enable) {
        flags = fcntl(sock, F_SETFL, flags | O_NONBLOCK);
    } else {
        flags = fcntl(sock, F_SETFL, flags & ~O_NONBLOCK);
    }
    if (flags == -1) {
        ecs_warn("http: failed to set socket NONBLOCK: %s",
            ecs_os_strerror(errno));
        return;
    }
#endif
}

static
int http_getnameinfo(
    const struct sockaddr* addr,
    ecs_size_t addr_len,
    char *host,
    ecs_size_t host_len,
    char *port,
    ecs_size_t port_len,
    int flags)
{
    ecs_assert(addr_len > 0, ECS_INTERNAL_ERROR, NULL);
    ecs_assert(host_len > 0, ECS_INTERNAL_ERROR, NULL);
    ecs_assert(port_len > 0, ECS_INTERNAL_ERROR, NULL);
    return getnameinfo(addr, (uint32_t)addr_len, host, (uint32_t)host_len, 
        port, (uint32_t)port_len, flags);
}

static
int http_bind(
    ecs_http_socket_t sock,
    const struct sockaddr* addr,
    ecs_size_t addr_len)
{
    ecs_assert(addr_len > 0, ECS_INTERNAL_ERROR, NULL);
    return bind(sock, addr, (uint32_t)addr_len);
}

static
bool http_socket_is_valid(
    ecs_http_socket_t sock)
{
#if defined(ECS_TARGET_WINDOWS)
    return sock != INVALID_SOCKET;
#else
    return sock >= 0;
#endif
}

#if defined(ECS_TARGET_WINDOWS)
#define HTTP_SOCKET_INVALID INVALID_SOCKET
#else
#define HTTP_SOCKET_INVALID (-1)
#endif

static
void http_close(
    ecs_http_socket_t *sock)
{
    ecs_assert(sock != NULL, ECS_INTERNAL_ERROR, NULL);

#if defined(ECS_TARGET_WINDOWS)
    closesocket(*sock);
#else
    ecs_dbg_2("http: closing socket %u", *sock);
    shutdown(*sock, SHUT_RDWR);
    close(*sock);
#endif
    *sock = HTTP_SOCKET_INVALID;
}

static
ecs_http_socket_t http_accept(
    ecs_http_socket_t sock,
    struct sockaddr* addr,
    ecs_size_t *addr_len)
{
    socklen_t len = (socklen_t)addr_len[0];
    ecs_http_socket_t result = accept(sock, addr, &len);
    addr_len[0] = (ecs_size_t)len;
    return result;
}

static
void http_reply_fini(ecs_http_reply_t* reply) {
    ecs_assert(reply != NULL, ECS_INTERNAL_ERROR, NULL);
    ecs_os_free(reply->body.content);
}

static
void http_request_fini(ecs_http_request_impl_t *req) {
    ecs_assert(req != NULL, ECS_INTERNAL_ERROR, NULL);
    ecs_assert(req->pub.conn != NULL, ECS_INTERNAL_ERROR, NULL);
    ecs_assert(req->pub.conn->server != NULL, ECS_INTERNAL_ERROR, NULL);
    ecs_assert(req->pub.conn->id == req->conn_id, ECS_INTERNAL_ERROR, NULL);
    ecs_os_free(req->res);
    flecs_sparse_remove_t(&req->pub.conn->server->requests, 
        ecs_http_request_impl_t, req->pub.id);
}

static
void http_connection_free(ecs_http_connection_impl_t *conn) {
    ecs_assert(conn != NULL, ECS_INTERNAL_ERROR, NULL);
    ecs_assert(conn->pub.id != 0, ECS_INTERNAL_ERROR, NULL);
    uint64_t conn_id = conn->pub.id;

    if (http_socket_is_valid(conn->sock)) {
        http_close(&conn->sock);
    }

    flecs_sparse_remove_t(&conn->pub.server->connections, 
        ecs_http_connection_impl_t, conn_id);
}

// https://stackoverflow.com/questions/10156409/convert-hex-string-char-to-int
static
char http_hex_2_int(char a, char b){
    a = (a <= '9') ? (char)(a - '0') : (char)((a & 0x7) + 9);
    b = (b <= '9') ? (char)(b - '0') : (char)((b & 0x7) + 9);
    return (char)((a << 4) + b);
}

static
void http_decode_url_str(
    char *str) 
{
    char ch, *ptr, *dst = str;
    for (ptr = str; (ch = *ptr); ptr++) {
        if (ch == '%') {
            dst[0] = http_hex_2_int(ptr[1], ptr[2]);
            dst ++;
            ptr += 2;
        } else {
            dst[0] = ptr[0];
            dst ++;
        }
    }
    dst[0] = '\0';
}

static
void http_parse_method(
    ecs_http_fragment_t *frag)
{
    char *method = ecs_strbuf_get_small(&frag->buf);
    if (!ecs_os_strcmp(method, "GET")) frag->method = EcsHttpGet;
    else if (!ecs_os_strcmp(method, "POST")) frag->method = EcsHttpPost;
    else if (!ecs_os_strcmp(method, "PUT")) frag->method = EcsHttpPut;
    else if (!ecs_os_strcmp(method, "DELETE")) frag->method = EcsHttpDelete;
    else if (!ecs_os_strcmp(method, "OPTIONS")) frag->method = EcsHttpOptions;
    else {
        frag->method = EcsHttpMethodUnsupported;
        frag->invalid = true;
    }
    ecs_strbuf_reset(&frag->buf);
}

static
bool http_header_writable(
    ecs_http_fragment_t *frag)
{
    return frag->header_count < ECS_HTTP_HEADER_COUNT_MAX;
}

static
void http_header_buf_reset(
    ecs_http_fragment_t *frag)
{
    frag->header_buf[0] = '\0';
    frag->header_buf_ptr = frag->header_buf;
}

static
void http_header_buf_append(
    ecs_http_fragment_t *frag,
    char ch)
{
    if ((frag->header_buf_ptr - frag->header_buf) < 
        ECS_SIZEOF(frag->header_buf)) 
    {
        frag->header_buf_ptr[0] = ch;
        frag->header_buf_ptr ++;
    } else {
        frag->header_buf_ptr[0] = '\0';
    }
}

static
uint64_t http_request_key_hash(const void *ptr) {
    const ecs_http_request_key_t *key = ptr;
    const char *array = key->array;
    int32_t count = key->count;
    return flecs_hash(array, count * ECS_SIZEOF(char));
}

static
int http_request_key_compare(const void *ptr_1, const void *ptr_2) {
    const ecs_http_request_key_t *type_1 = ptr_1;
    const ecs_http_request_key_t *type_2 = ptr_2;

    int32_t count_1 = type_1->count;
    int32_t count_2 = type_2->count;

    if (count_1 != count_2) {
        return (count_1 > count_2) - (count_1 < count_2);
    }

    return ecs_os_memcmp(type_1->array, type_2->array, count_1);
}

static
ecs_http_request_entry_t* http_find_request_entry(
    ecs_http_server_t *srv,
    const char *array,
    int32_t count)
{
    ecs_http_request_key_t key;
    key.array = array;
    key.count = count;

    ecs_time_t t = {0, 0};
    ecs_http_request_entry_t *entry = flecs_hashmap_get(
        &srv->request_cache, &key, ecs_http_request_entry_t);

    if (entry) {
        ecs_ftime_t tf = (ecs_ftime_t)ecs_time_measure(&t);
        if ((tf - entry->time) < ECS_HTTP_CACHE_TIMEOUT) {
            return entry;
        }
    }
    return NULL;
}

static
void http_insert_request_entry(
    ecs_http_server_t *srv,
    ecs_http_request_impl_t *req,
    ecs_http_reply_t *reply)
{
    int32_t content_length = ecs_strbuf_written(&reply->body);
    if (!content_length) {
        return;
    }

    ecs_http_request_key_t key;
    key.array = req->res;
    key.count = req->req_len;
    ecs_http_request_entry_t *entry = flecs_hashmap_get(
        &srv->request_cache, &key, ecs_http_request_entry_t);
    if (!entry) {
        flecs_hashmap_result_t elem = flecs_hashmap_ensure(
            &srv->request_cache, &key, ecs_http_request_entry_t);
        ecs_http_request_key_t *elem_key = elem.key;
        elem_key->array = ecs_os_memdup_n(key.array, char, key.count);
        entry = elem.value;
    } else {
        ecs_os_free(entry->content);
    }

    ecs_time_t t = {0, 0};
    entry->time = (ecs_ftime_t)ecs_time_measure(&t);
    entry->content_length = ecs_strbuf_written(&reply->body);
    entry->content = ecs_strbuf_get(&reply->body);
    ecs_strbuf_appendstrn(&reply->body, 
            entry->content, entry->content_length);
}

static
char* http_decode_request(
    ecs_http_request_impl_t *req,
    ecs_http_fragment_t *frag)
{
    ecs_os_zeromem(req);

    char *res = ecs_strbuf_get(&frag->buf);
    if (!res) {
        return NULL;
    }

    req->pub.method = frag->method;
    req->pub.path = res + 1;
    http_decode_url_str(req->pub.path);

    if (frag->body_offset) {
        req->pub.body = &res[frag->body_offset];
    }
    int32_t i, count = frag->header_count;
    for (i = 0; i < count; i ++) {
        req->pub.headers[i].key = &res[frag->header_offsets[i]];
        req->pub.headers[i].value = &res[frag->header_value_offsets[i]];
    }
    count = frag->param_count;
    for (i = 0; i < count; i ++) {
        req->pub.params[i].key = &res[frag->param_offsets[i]];
        req->pub.params[i].value = &res[frag->param_value_offsets[i]];
        http_decode_url_str((char*)req->pub.params[i].value);
    }

    req->pub.header_count = frag->header_count;
    req->pub.param_count = frag->param_count;
    req->res = res;
    req->req_len = frag->header_offsets[0];

    return res;
}

static
ecs_http_request_entry_t* http_enqueue_request(
    ecs_http_connection_impl_t *conn,
    uint64_t conn_id,
    ecs_http_fragment_t *frag)
{
    ecs_http_server_t *srv = conn->pub.server;

    ecs_os_mutex_lock(srv->lock);
    bool is_alive = conn->pub.id == conn_id;

    if (!is_alive || frag->invalid) { 
        /* Don't enqueue invalid requests or requests for purged connections */
        ecs_strbuf_reset(&frag->buf);
    } else {
        ecs_http_request_impl_t req;
        char *res = http_decode_request(&req, frag);
        if (res) {
            req.pub.conn = (ecs_http_connection_t*)conn;

            /* Check cache for GET requests */
            if (frag->method == EcsHttpGet) {
                ecs_http_request_entry_t *entry = 
                    http_find_request_entry(srv, res, frag->header_offsets[0]);
                if (entry) {
                    /* If an entry is found, don't enqueue a request. Instead
                     * return the cached response immediately. */
                    ecs_os_free(res);
                    return entry;
                }
            }

            ecs_http_request_impl_t *req_ptr = flecs_sparse_add_t(
                &srv->requests, ecs_http_request_impl_t);
            *req_ptr = req;
            req_ptr->pub.id = flecs_sparse_last_id(&srv->requests);
            req_ptr->conn_id = conn->pub.id;
            ecs_os_linc(&ecs_http_request_received_count);
        }
    }

    ecs_os_mutex_unlock(srv->lock);
    return NULL;
}

static
bool http_parse_request(
    ecs_http_fragment_t *frag,
    const char* req_frag, 
    ecs_size_t req_frag_len) 
{
    int32_t i;
    for (i = 0; i < req_frag_len; i++) {
        char c = req_frag[i];
        switch (frag->state) {
        case HttpFragStateBegin:
            ecs_os_memset_t(frag, 0, ecs_http_fragment_t);
            frag->buf.max = ECS_HTTP_METHOD_LEN_MAX;
            frag->state = HttpFragStateMethod;
            frag->header_buf_ptr = frag->header_buf;
            /* fallthrough */
        case HttpFragStateMethod:
            if (c == ' ') {
                http_parse_method(frag);
                frag->state = HttpFragStatePath;
                frag->buf.max = ECS_HTTP_REQUEST_LEN_MAX;
            } else {
                ecs_strbuf_appendch(&frag->buf, c);
            }
            break;
        case HttpFragStatePath:
            if (c == ' ') {
                frag->state = HttpFragStateVersion;
                ecs_strbuf_appendch(&frag->buf, '\0');
            } else {
                if (c == '?' || c == '=' || c == '&') {
                    ecs_strbuf_appendch(&frag->buf, '\0');
                    int32_t offset = ecs_strbuf_written(&frag->buf);
                    if (c == '?' || c == '&') {
                        frag->param_offsets[frag->param_count] = offset;
                    } else {
                        frag->param_value_offsets[frag->param_count] = offset;
                        frag->param_count ++;
                    }
                } else {
                    ecs_strbuf_appendch(&frag->buf, c);
                }
            }
            break;
        case HttpFragStateVersion:
            if (c == '\r') {
                frag->state = HttpFragStateCR;
            } /* version is not stored */
            break;
        case HttpFragStateHeaderStart:
            if (http_header_writable(frag)) {
                frag->header_offsets[frag->header_count] = 
                    ecs_strbuf_written(&frag->buf);
            }
            http_header_buf_reset(frag);
            frag->state = HttpFragStateHeaderName;
            /* fallthrough */
        case HttpFragStateHeaderName:
            if (c == ':') {
                frag->state = HttpFragStateHeaderValueStart;
                http_header_buf_append(frag, '\0');
                frag->parse_content_length = !ecs_os_strcmp(
                    frag->header_buf, "Content-Length");

                if (http_header_writable(frag)) {
                    ecs_strbuf_appendch(&frag->buf, '\0');
                    frag->header_value_offsets[frag->header_count] =
                        ecs_strbuf_written(&frag->buf);
                }
            } else if (c == '\r') {
                frag->state = HttpFragStateCR;
            } else  {
                http_header_buf_append(frag, c);
                if (http_header_writable(frag)) {
                    ecs_strbuf_appendch(&frag->buf, c);
                }
            }
            break;
        case HttpFragStateHeaderValueStart:
            http_header_buf_reset(frag);
            frag->state = HttpFragStateHeaderValue;
            if (c == ' ') { /* skip first space */
                break;
            }
            /* fallthrough */
        case HttpFragStateHeaderValue:
            if (c == '\r') {
                if (frag->parse_content_length) {
                    http_header_buf_append(frag, '\0');
                    int32_t len = atoi(frag->header_buf);
                    if (len < 0) {
                        frag->invalid = true;
                    } else {
                        frag->content_length = len;
                    }
                    frag->parse_content_length = false;
                }
                if (http_header_writable(frag)) {
                    int32_t cur = ecs_strbuf_written(&frag->buf);
                    if (frag->header_offsets[frag->header_count] < cur &&
                        frag->header_value_offsets[frag->header_count] < cur)
                    {
                        ecs_strbuf_appendch(&frag->buf, '\0');
                        frag->header_count ++;
                    }
                }
                frag->state = HttpFragStateCR;
            } else {
                if (frag->parse_content_length) {
                    http_header_buf_append(frag, c);
                }
                if (http_header_writable(frag)) {
                    ecs_strbuf_appendch(&frag->buf, c);
                }
            }
            break;
        case HttpFragStateCR:
            if (c == '\n') {
                frag->state = HttpFragStateCRLF;
            } else {
                frag->state = HttpFragStateHeaderStart;
            }
            break;
        case HttpFragStateCRLF:
            if (c == '\r') {
                frag->state = HttpFragStateCRLFCR;
            } else {
                frag->state = HttpFragStateHeaderStart;
                i--;
            }
            break;
        case HttpFragStateCRLFCR:
            if (c == '\n') {
                if (frag->content_length != 0) {
                    frag->body_offset = ecs_strbuf_written(&frag->buf);
                    frag->state = HttpFragStateBody;
                } else {
                    frag->state = HttpFragStateDone;
                }
            } else {
                frag->state = HttpFragStateHeaderStart;
            }
            break;
        case HttpFragStateBody: {
                ecs_strbuf_appendch(&frag->buf, c);
                if ((ecs_strbuf_written(&frag->buf) - frag->body_offset) == 
                    frag->content_length) 
                {
                    frag->state = HttpFragStateDone;
                }
            }
            break;
        case HttpFragStateDone:
            break;
        }
    }

    if (frag->state == HttpFragStateDone) {
        return true;
    } else {
        return false;
    }
}

static
ecs_http_send_request_t* http_send_queue_post(
    ecs_http_server_t *srv)
{
    /* This function should only be called while the server is locked. Before 
     * the lock is released, the returned element should be populated. */
    ecs_http_send_queue_t *sq = &srv->send_queue;
    int32_t next = (sq->head + 1) % ECS_HTTP_SEND_QUEUE_MAX;
    if (next == sq->tail) {
        return NULL;
    }

    /* Don't enqueue new requests if server is shutting down */
    if (!srv->should_run) {
        return NULL;
    }

    /* Return element at end of the queue */
    ecs_http_send_request_t *result = &sq->requests[sq->head];
    sq->head = next;
    return result;
}

static
ecs_http_send_request_t* http_send_queue_get(
    ecs_http_server_t *srv)
{
    ecs_os_mutex_lock(srv->lock);
    ecs_http_send_queue_t *sq = &srv->send_queue;
    if (sq->tail == sq->head) {
        return NULL;
    }

    int32_t next = (sq->tail + 1) % ECS_HTTP_SEND_QUEUE_MAX;
    ecs_http_send_request_t *result = &sq->requests[sq->tail];
    sq->tail = next;
    return result;
}

static
void* http_server_send_queue(void* arg) {
    ecs_http_server_t *srv = arg;
    int32_t wait_ms = srv->send_queue.wait_ms;

    /* Run for as long as the server is running or there are messages. When the
     * server is stopping, no new messages will be enqueued */
    while (srv->should_run || (srv->send_queue.head != srv->send_queue.tail)) {
        ecs_http_send_request_t* r = http_send_queue_get(srv);
        if (!r) {
            ecs_os_mutex_unlock(srv->lock);
            /* If the queue is empty, wait so we don't run too fast */
            if (srv->should_run) {
                ecs_os_sleep(0, wait_ms * 1000 * 1000);
            }
        } else {
            ecs_http_socket_t sock = r->sock;
            char *headers = r->headers;
            int32_t headers_length = r->header_length;
            char *content = r->content;
            int32_t content_length = r->content_length;
            ecs_os_mutex_unlock(srv->lock);

            if (http_socket_is_valid(sock)) {
                bool error = false;

                http_sock_nonblock(sock, false);

                /* Write headers */
                ecs_size_t written = http_send(sock, headers, headers_length, 0);
                if (written != headers_length) {
                    ecs_err("http: failed to write HTTP response headers: %s",
                        ecs_os_strerror(errno));
                    ecs_os_linc(&ecs_http_send_error_count);
                    error = true;
                } else if (content_length >= 0) {
                    /* Write content */
                    written = http_send(sock, content, content_length, 0);
                    if (written != content_length) {
                        ecs_err("http: failed to write HTTP response body: %s",
                            ecs_os_strerror(errno));
                        ecs_os_linc(&ecs_http_send_error_count);
                        error = true;
                    }
                }
                if (!error) {
                    ecs_os_linc(&ecs_http_send_ok_count);
                }

                http_close(&sock);
            } else {
                ecs_err("http: invalid socket\n");
            }

            ecs_os_free(content);
            ecs_os_free(headers);
        }
    }
    return NULL;
}

static
void http_append_send_headers(
    ecs_strbuf_t *hdrs,
    int code, 
    const char* status, 
    const char* content_type,  
    ecs_strbuf_t *extra_headers,
    ecs_size_t content_len,
    bool preflight)
{
    ecs_strbuf_appendlit(hdrs, "HTTP/1.1 ");
    ecs_strbuf_appendint(hdrs, code);
    ecs_strbuf_appendch(hdrs, ' ');
    ecs_strbuf_appendstr(hdrs, status);
    ecs_strbuf_appendlit(hdrs, "\r\n");

    if (content_type) {
        ecs_strbuf_appendlit(hdrs, "Content-Type: ");
        ecs_strbuf_appendstr(hdrs, content_type);
        ecs_strbuf_appendlit(hdrs, "\r\n");
    }

    if (content_len >= 0) {
        ecs_strbuf_appendlit(hdrs, "Content-Length: ");
        ecs_strbuf_append(hdrs, "%d", content_len);
        ecs_strbuf_appendlit(hdrs, "\r\n");
    }

    ecs_strbuf_appendlit(hdrs, "Access-Control-Allow-Origin: *\r\n");
    if (preflight) {
        ecs_strbuf_appendlit(hdrs, "Access-Control-Allow-Private-Network: true\r\n");
        ecs_strbuf_appendlit(hdrs, "Access-Control-Allow-Methods: GET, PUT, OPTIONS\r\n");
        ecs_strbuf_appendlit(hdrs, "Access-Control-Max-Age: 600\r\n");
    }

    ecs_strbuf_mergebuff(hdrs, extra_headers);

    ecs_strbuf_appendlit(hdrs, "\r\n");
}

static
void http_send_reply(
    ecs_http_connection_impl_t* conn, 
    ecs_http_reply_t* reply,
    bool preflight) 
{
    ecs_strbuf_t hdrs = ECS_STRBUF_INIT;
    char *content = ecs_strbuf_get(&reply->body);
    int32_t content_length = reply->body.length - 1;

    /* Use asynchronous send queue for outgoing data so send operations won't
     * hold up main thread */
    ecs_http_send_request_t *req = NULL;

    if (!preflight) {
        req = http_send_queue_post(conn->pub.server);
        if (!req) {
            reply->code = 503; /* queue full, server is busy */
            ecs_os_linc(&ecs_http_busy_count);
        }
    }

    http_append_send_headers(&hdrs, reply->code, reply->status, 
        reply->content_type, &reply->headers, content_length, preflight);
    char *headers = ecs_strbuf_get(&hdrs);
    ecs_size_t headers_length = ecs_strbuf_written(&hdrs);

    if (!req) {
        ecs_size_t written = http_send(conn->sock, headers, headers_length, 0);
        if (written != headers_length) {
            ecs_err("http: failed to send reply to '%s:%s': %s",
                conn->pub.host, conn->pub.port, ecs_os_strerror(errno));
            ecs_os_linc(&ecs_http_send_error_count);
        }
        ecs_os_free(content);
        ecs_os_free(headers);
        http_close(&conn->sock);
        return;
    }

    /* Second, enqueue send request for response body */
    req->sock = conn->sock;
    req->headers = headers;
    req->header_length = headers_length;
    req->content = content;
    req->content_length = content_length;

    /* Take ownership of values */
    reply->body.content = NULL;
    conn->sock = HTTP_SOCKET_INVALID;
}

static
void http_recv_connection(
    ecs_http_server_t *srv,
    ecs_http_connection_impl_t *conn, 
    uint64_t conn_id,
    ecs_http_socket_t sock)
{
    ecs_size_t bytes_read;
    char recv_buf[ECS_HTTP_SEND_RECV_BUFFER_SIZE];
    ecs_http_fragment_t frag = {0};
    int32_t retries = 0;

    do {
        if ((bytes_read = http_recv(
            sock, recv_buf, ECS_SIZEOF(recv_buf), 0)) > 0) 
        {
            bool is_alive = conn->pub.id == conn_id;
            if (!is_alive) {
                /* Connection has been purged by main thread */
                goto done;
            }

            if (http_parse_request(&frag, recv_buf, bytes_read)) {
                if (frag.method == EcsHttpOptions) {
                    ecs_http_reply_t reply;
                    reply.body = ECS_STRBUF_INIT;
                    reply.code = 200;
                    reply.content_type = NULL;
                    reply.headers = ECS_STRBUF_INIT;
                    reply.status = "OK";
                    http_send_reply(conn, &reply, true);
                    ecs_os_linc(&ecs_http_request_preflight_count);
                } else {
                    ecs_http_request_entry_t *entry =
                        http_enqueue_request(conn, conn_id, &frag);
                    if (entry) {
                        ecs_http_reply_t reply;
                        reply.body = ECS_STRBUF_INIT;
                        reply.code = 200;
                        reply.content_type = NULL;
                        reply.headers = ECS_STRBUF_INIT;
                        reply.status = "OK";
                        ecs_strbuf_appendstrn(&reply.body, 
                            entry->content, entry->content_length);
                        http_send_reply(conn, &reply, false);
                        http_connection_free(conn);

                        /* Lock was transferred from enqueue_request */
                        ecs_os_mutex_unlock(srv->lock);
                    }
                }
            } else {
                ecs_os_linc(&ecs_http_request_invalid_count);
            }
        }

        ecs_os_sleep(0, 10 * 1000 * 1000);
    } while ((bytes_read == -1) && (++retries < ECS_HTTP_REQUEST_RECV_RETRY));

    if (retries == ECS_HTTP_REQUEST_RECV_RETRY) {
        http_close(&sock);
    }

done:
    ecs_strbuf_reset(&frag.buf);
}

typedef struct {
    ecs_http_connection_impl_t *conn;
    uint64_t id;
} http_conn_res_t;

static
http_conn_res_t http_init_connection(
    ecs_http_server_t *srv, 
    ecs_http_socket_t sock_conn,
    struct sockaddr_storage *remote_addr, 
    ecs_size_t remote_addr_len) 
{
    http_sock_set_timeout(sock_conn, 100);
    http_sock_keep_alive(sock_conn);
    http_sock_nonblock(sock_conn, true);

    /* Create new connection */
    ecs_os_mutex_lock(srv->lock);
    ecs_http_connection_impl_t *conn = flecs_sparse_add_t(
        &srv->connections, ecs_http_connection_impl_t);
    uint64_t conn_id = conn->pub.id = flecs_sparse_last_id(&srv->connections);
    conn->pub.server = srv;
    conn->sock = sock_conn;
    ecs_os_mutex_unlock(srv->lock);

    char *remote_host = conn->pub.host;
    char *remote_port = conn->pub.port;

    /* Fetch name & port info */
    if (http_getnameinfo((struct sockaddr*) remote_addr, remote_addr_len,
        remote_host, ECS_SIZEOF(conn->pub.host),
        remote_port, ECS_SIZEOF(conn->pub.port),
            NI_NUMERICHOST | NI_NUMERICSERV))
    {
        ecs_os_strcpy(remote_host, "unknown");
        ecs_os_strcpy(remote_port, "unknown");
    }

    ecs_dbg_2("http: connection established from '%s:%s' (socket %u)", 
        remote_host, remote_port, sock_conn);
    
    return (http_conn_res_t){ .conn = conn, .id = conn_id };
}

static
void http_accept_connections(
    ecs_http_server_t* srv, 
    const struct sockaddr* addr, 
    ecs_size_t addr_len) 
{
#ifdef ECS_TARGET_WINDOWS
    /* If on Windows, test if winsock needs to be initialized */
    SOCKET testsocket = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
    if (SOCKET_ERROR == testsocket && WSANOTINITIALISED == WSAGetLastError()) {
        WSADATA data = { 0 };
        int result = WSAStartup(MAKEWORD(2, 2), &data);
        if (result) {
            ecs_warn("http: WSAStartup failed with GetLastError = %d\n", 
                GetLastError());
            return;
        }
    } else {
        http_close(&testsocket);
    }
#endif

    /* Resolve name + port (used for logging) */
    char addr_host[256];
    char addr_port[20];

    ecs_http_socket_t sock = HTTP_SOCKET_INVALID;
    ecs_assert(srv->sock == HTTP_SOCKET_INVALID, ECS_INTERNAL_ERROR, NULL);

    if (http_getnameinfo(
        addr, addr_len, addr_host, ECS_SIZEOF(addr_host), addr_port, 
        ECS_SIZEOF(addr_port), NI_NUMERICHOST | NI_NUMERICSERV))
    {
        ecs_os_strcpy(addr_host, "unknown");
        ecs_os_strcpy(addr_port, "unknown");
    }

    ecs_os_mutex_lock(srv->lock);
    if (srv->should_run) {
        ecs_dbg_2("http: initializing connection socket");

        sock = socket(addr->sa_family, SOCK_STREAM, IPPROTO_TCP);
        if (!http_socket_is_valid(sock)) {
            ecs_err("http: unable to create new connection socket: %s", 
                ecs_os_strerror(errno));
            ecs_os_mutex_unlock(srv->lock);
            goto done;
        }

        int reuse = 1, result;
        result = setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, 
            (char*)&reuse, ECS_SIZEOF(reuse)); 
        if (result) {
            ecs_warn("http: failed to setsockopt: %s", ecs_os_strerror(errno));
        }

        if (addr->sa_family == AF_INET6) {
            int ipv6only = 0;
            if (setsockopt(sock, IPPROTO_IPV6, IPV6_V6ONLY, 
                (char*)&ipv6only, ECS_SIZEOF(ipv6only)))
            {
                ecs_warn("http: failed to setsockopt: %s", 
                    ecs_os_strerror(errno));
            }
        }

        result = http_bind(sock, addr, addr_len);
        if (result) {
            ecs_err("http: failed to bind to '%s:%s': %s", 
                addr_host, addr_port, ecs_os_strerror(errno));
            ecs_os_mutex_unlock(srv->lock);
            goto done;
        }

        http_sock_set_timeout(sock, 1000);

        srv->sock = sock;

        result = listen(srv->sock, SOMAXCONN);
        if (result) {
            ecs_warn("http: could not listen for SOMAXCONN (%d) connections: %s", 
                SOMAXCONN, ecs_os_strerror(errno));
        }

        ecs_trace("http: listening for incoming connections on '%s:%s'",
            addr_host, addr_port);
    } else {
        ecs_dbg_2("http: server shut down while initializing");
    }
    ecs_os_mutex_unlock(srv->lock);

    struct sockaddr_storage remote_addr;
    ecs_size_t remote_addr_len = 0;

    while (srv->should_run) {
        remote_addr_len = ECS_SIZEOF(remote_addr);
        ecs_http_socket_t sock_conn = http_accept(srv->sock, (struct sockaddr*) &remote_addr, 
            &remote_addr_len);

        if (!http_socket_is_valid(sock_conn)) {
            if (srv->should_run) {
                ecs_dbg("http: connection attempt failed: %s", 
                    ecs_os_strerror(errno));
            }
            continue;
        }

        http_conn_res_t conn = http_init_connection(srv, sock_conn, &remote_addr, remote_addr_len);
        http_recv_connection(srv, conn.conn, conn.id, sock_conn);
    }

done:
    ecs_os_mutex_lock(srv->lock);
    if (http_socket_is_valid(sock) && errno != EBADF) {
        http_close(&sock);
        srv->sock = sock;
    }
    ecs_os_mutex_unlock(srv->lock);

    ecs_trace("http: no longer accepting connections on '%s:%s'",
        addr_host, addr_port);
}

static
void* http_server_thread(void* arg) {
    ecs_http_server_t *srv = arg;
    struct sockaddr_in addr;
    ecs_os_zeromem(&addr);
    addr.sin_family = AF_INET;
    addr.sin_port = htons(srv->port);

    if (!srv->ipaddr) {
        addr.sin_addr.s_addr = htonl(INADDR_ANY);
    } else {
        inet_pton(AF_INET, srv->ipaddr, &(addr.sin_addr));
    }

    http_accept_connections(srv, (struct sockaddr*)&addr, ECS_SIZEOF(addr));
    return NULL;
}

static
void http_do_request(
    ecs_http_server_t *srv,
    ecs_http_reply_t *reply,
    const ecs_http_request_impl_t *req)
{
    if (srv->callback((ecs_http_request_t*)req, reply, srv->ctx) == false) {
        reply->code = 404;
        reply->status = "Resource not found";
        ecs_os_linc(&ecs_http_request_not_handled_count);
    } else {
        if (reply->code >= 400) {
            ecs_os_linc(&ecs_http_request_handled_error_count);
        } else {
            ecs_os_linc(&ecs_http_request_handled_ok_count);
        }
    }
}

static
void http_handle_request(
    ecs_http_server_t *srv,
    ecs_http_request_impl_t *req)
{
    ecs_http_reply_t reply = ECS_HTTP_REPLY_INIT;
    ecs_http_connection_impl_t *conn = 
        (ecs_http_connection_impl_t*)req->pub.conn;

    if (req->pub.method != EcsHttpOptions) {
        if (srv->callback((ecs_http_request_t*)req, &reply, srv->ctx) == false) {
            reply.code = 404;
            reply.status = "Resource not found";
            ecs_os_linc(&ecs_http_request_not_handled_count);
        } else {
            if (reply.code >= 400) {
                ecs_os_linc(&ecs_http_request_handled_error_count);
            } else {
                ecs_os_linc(&ecs_http_request_handled_ok_count);
            }
        }

        if (req->pub.method == EcsHttpGet) {
            http_insert_request_entry(srv, req, &reply);
        }

        http_send_reply(conn, &reply, false);
        ecs_dbg_2("http: reply sent to '%s:%s'", conn->pub.host, conn->pub.port);
    } else {
        /* Already taken care of */
    }

    http_reply_fini(&reply);
    http_request_fini(req);
    http_connection_free(conn);
}

static
void http_purge_request_cache(
    ecs_http_server_t *srv,
    bool fini)
{
    ecs_time_t t = {0, 0};
    ecs_ftime_t time = (ecs_ftime_t)ecs_time_measure(&t);
    ecs_map_iter_t it = ecs_map_iter(&srv->request_cache.impl);
    while (ecs_map_next(&it)) {
        ecs_hm_bucket_t *bucket = ecs_map_ptr(&it);
        int32_t i, count = ecs_vec_count(&bucket->values);
        ecs_http_request_key_t *keys = ecs_vec_first(&bucket->keys);
        ecs_http_request_entry_t *entries = ecs_vec_first(&bucket->values);
        for (i = count - 1; i >= 0; i --) {
            ecs_http_request_entry_t *entry = &entries[i];
            if (fini || ((time - entry->time) > ECS_HTTP_CACHE_PURGE_TIMEOUT)) {
                ecs_http_request_key_t *key = &keys[i];
                ecs_os_free((char*)key->array);
                ecs_os_free(entry->content);
                flecs_hm_bucket_remove(&srv->request_cache, bucket, 
                    ecs_map_key(&it), i);
            }
        }
    }

    if (fini) {
        flecs_hashmap_fini(&srv->request_cache);
    }
}

static
int32_t http_dequeue_requests(
    ecs_http_server_t *srv,
    double delta_time)
{
    ecs_os_mutex_lock(srv->lock);

    int32_t i, request_count = flecs_sparse_count(&srv->requests);
    for (i = request_count - 1; i >= 1; i --) {
        ecs_http_request_impl_t *req = flecs_sparse_get_dense_t(
            &srv->requests, ecs_http_request_impl_t, i);
        http_handle_request(srv, req);
    }

    int32_t connections_count = flecs_sparse_count(&srv->connections);
    for (i = connections_count - 1; i >= 1; i --) {
        ecs_http_connection_impl_t *conn = flecs_sparse_get_dense_t(
            &srv->connections, ecs_http_connection_impl_t, i);

        conn->dequeue_timeout += delta_time;
        conn->dequeue_retries ++;
        
        if ((conn->dequeue_timeout > 
            (double)ECS_HTTP_CONNECTION_PURGE_TIMEOUT) &&
             (conn->dequeue_retries > ECS_HTTP_CONNECTION_PURGE_RETRY_COUNT)) 
        {
            ecs_dbg("http: purging connection '%s:%s' (sock = %d)", 
                conn->pub.host, conn->pub.port, conn->sock);
            http_connection_free(conn);
        }
    }

    http_purge_request_cache(srv, false);
    ecs_os_mutex_unlock(srv->lock);

    return request_count - 1;
}

const char* ecs_http_get_header(
    const ecs_http_request_t* req,
    const char* name) 
{
    for (ecs_size_t i = 0; i < req->header_count; i++) {
        if (!ecs_os_strcmp(req->headers[i].key, name)) {
            return req->headers[i].value;
        }
    }
    return NULL;
}

const char* ecs_http_get_param(
    const ecs_http_request_t* req,
    const char* name) 
{
    for (ecs_size_t i = 0; i < req->param_count; i++) {
        if (!ecs_os_strcmp(req->params[i].key, name)) {
            return req->params[i].value;
        }
    }
    return NULL;
}

ecs_http_server_t* ecs_http_server_init(
    const ecs_http_server_desc_t *desc) 
{
    ecs_check(ecs_os_has_threading(), ECS_UNSUPPORTED, 
        "missing OS API implementation");

    ecs_http_server_t* srv = ecs_os_calloc_t(ecs_http_server_t);
    srv->lock = ecs_os_mutex_new();
    srv->sock = HTTP_SOCKET_INVALID;

    srv->should_run = false;
    srv->initialized = true;

    srv->callback = desc->callback;
    srv->ctx = desc->ctx;
    srv->port = desc->port;
    srv->ipaddr = desc->ipaddr;
    srv->send_queue.wait_ms = desc->send_queue_wait_ms;
    if (!srv->send_queue.wait_ms) {
        srv->send_queue.wait_ms = 1;
    }

    flecs_sparse_init_t(&srv->connections, NULL, NULL, ecs_http_connection_impl_t);
    flecs_sparse_init_t(&srv->requests, NULL, NULL, ecs_http_request_impl_t);

    /* Start at id 1 */
    flecs_sparse_new_id(&srv->connections);
    flecs_sparse_new_id(&srv->requests);

    /* Initialize request cache */
    flecs_hashmap_init(&srv->request_cache, 
        ecs_http_request_key_t, ecs_http_request_entry_t,
        http_request_key_hash, http_request_key_compare, NULL);

#ifndef ECS_TARGET_WINDOWS
    /* Ignore pipe signal. SIGPIPE can occur when a message is sent to a client
     * but te client already disconnected. */
    signal(SIGPIPE, SIG_IGN);
#endif

    return srv;
error:
    return NULL;
}

void ecs_http_server_fini(
    ecs_http_server_t* srv) 
{
    if (srv->should_run) {
        ecs_http_server_stop(srv);
    }
    ecs_os_mutex_free(srv->lock);
    http_purge_request_cache(srv, true);
    flecs_sparse_fini(&srv->requests);
    flecs_sparse_fini(&srv->connections);
    ecs_os_free(srv);
}

int ecs_http_server_start(
    ecs_http_server_t *srv)
{
    ecs_check(srv != NULL, ECS_INVALID_PARAMETER, NULL);
    ecs_check(srv->initialized, ECS_INVALID_PARAMETER, NULL);
    ecs_check(!srv->should_run, ECS_INVALID_PARAMETER, NULL);
    ecs_check(!srv->thread, ECS_INVALID_PARAMETER, NULL);

    srv->should_run = true;

    ecs_dbg("http: starting server thread");

    srv->thread = ecs_os_thread_new(http_server_thread, srv);
    if (!srv->thread) {
        goto error;
    }

    srv->send_queue.thread = ecs_os_thread_new(http_server_send_queue, srv);
    if (!srv->send_queue.thread) {
        goto error;
    }

    return 0;
error:
    return -1;
}

void ecs_http_server_stop(
    ecs_http_server_t* srv) 
{
    ecs_check(srv != NULL, ECS_INVALID_PARAMETER, NULL);
    ecs_check(srv->initialized, ECS_INVALID_OPERATION, NULL);
    ecs_check(srv->should_run, ECS_INVALID_PARAMETER, NULL);

    /* Stop server thread */
    ecs_dbg("http: shutting down server thread");

    ecs_os_mutex_lock(srv->lock);
    srv->should_run = false;
    if (http_socket_is_valid(srv->sock)) {
        http_close(&srv->sock);
    }
    ecs_os_mutex_unlock(srv->lock);

    ecs_os_thread_join(srv->thread);
    ecs_os_thread_join(srv->send_queue.thread);
    ecs_trace("http: server threads shut down");

    /* Cleanup all outstanding requests */
    int i, count = flecs_sparse_count(&srv->requests);
    for (i = count - 1; i >= 1; i --) {
        http_request_fini(flecs_sparse_get_dense_t(
            &srv->requests, ecs_http_request_impl_t, i));
    }

    /* Close all connections */
    count = flecs_sparse_count(&srv->connections);
    for (i = count - 1; i >= 1; i --) {
        http_connection_free(flecs_sparse_get_dense_t(
            &srv->connections, ecs_http_connection_impl_t, i));
    }

    ecs_assert(flecs_sparse_count(&srv->connections) == 1, 
        ECS_INTERNAL_ERROR, NULL);
    ecs_assert(flecs_sparse_count(&srv->requests) == 1,
        ECS_INTERNAL_ERROR, NULL);

    srv->thread = 0;
error:
    return;
}

void ecs_http_server_dequeue(
    ecs_http_server_t* srv,
    ecs_ftime_t delta_time)
{
    ecs_check(srv != NULL, ECS_INVALID_PARAMETER, NULL);
    ecs_check(srv->initialized, ECS_INVALID_PARAMETER, NULL);
    ecs_check(srv->should_run, ECS_INVALID_PARAMETER, NULL);
    
    srv->dequeue_timeout += (double)delta_time;
    srv->stats_timeout += (double)delta_time;

    if ((1000 * srv->dequeue_timeout) > (double)ECS_HTTP_MIN_DEQUEUE_INTERVAL) {
        srv->dequeue_timeout = 0;

        ecs_time_t t = {0};
        ecs_time_measure(&t);
        int32_t request_count = http_dequeue_requests(srv, srv->dequeue_timeout);
        srv->requests_processed += request_count;
        srv->requests_processed_total += request_count;
        double time_spent = ecs_time_measure(&t);
        srv->request_time += time_spent;
        srv->request_time_total += time_spent;
        srv->dequeue_count ++;
    }

    if ((1000 * srv->stats_timeout) > (double)ECS_HTTP_MIN_STATS_INTERVAL) {
        srv->stats_timeout = 0;
        ecs_dbg("http: processed %d requests in %.3fs (avg %.3fs / dequeue)",
            srv->requests_processed, srv->request_time, 
            (srv->request_time / (double)srv->dequeue_count));
        srv->requests_processed = 0;
        srv->request_time = 0;
        srv->dequeue_count = 0;
    }

error:
    return;
}

int ecs_http_server_http_request(
    ecs_http_server_t* srv,
    const char *req,
    ecs_size_t len,
    ecs_http_reply_t *reply_out)
{
    if (!len) {
        len = ecs_os_strlen(req);
    }

    ecs_http_fragment_t frag = {0};
    if (!http_parse_request(&frag, req, len)) {
        ecs_strbuf_reset(&frag.buf);
        reply_out->code = 400;
        return -1;
    }

    ecs_http_request_impl_t request;
    char *res = http_decode_request(&request, &frag);
    if (!res) {
        reply_out->code = 400;
        return -1;
    }

    http_do_request(srv, reply_out, &request);
    ecs_os_free(res);

    return (reply_out->code >= 400) ? -1 : 0;
}

int ecs_http_server_request(
    ecs_http_server_t* srv,
    const char *method,
    const char *req,
    ecs_http_reply_t *reply_out)
{
    ecs_strbuf_t reqbuf = ECS_STRBUF_INIT;
    ecs_strbuf_appendstr_zerocpy_const(&reqbuf, method);
    ecs_strbuf_appendlit(&reqbuf, " ");
    ecs_strbuf_appendstr_zerocpy_const(&reqbuf, req);
    ecs_strbuf_appendlit(&reqbuf, " HTTP/1.1\r\n\r\n");
    int32_t len = ecs_strbuf_written(&reqbuf);
    char *reqstr = ecs_strbuf_get(&reqbuf);
    int result = ecs_http_server_http_request(srv, reqstr, len, reply_out);
    ecs_os_free(reqstr);
    return result;
}

void* ecs_http_server_ctx(
    ecs_http_server_t* srv)
{
    return srv->ctx;
}

#endif

 /**
 * @file addons/rules/compile.c
 * @brief Compile rule program from filter.
 */

 /**
 * @file addons/rules/rules.h
 * @brief Internal types and functions for rules addon.
 */


#ifdef FLECS_RULES

typedef uint8_t ecs_var_id_t;
typedef int16_t ecs_rule_lbl_t;
typedef ecs_flags64_t ecs_write_flags_t;

#define EcsRuleMaxVarCount      (64)
#define EcsVarNone              ((ecs_var_id_t)-1)
#define EcsThisName             "this"

/* -- Variable types -- */
typedef enum {
    EcsVarEntity,          /* Variable that stores an entity id */
    EcsVarTable,           /* Variable that stores a table */
    EcsVarAny              /* Used when requesting either entity or table var */
} ecs_var_kind_t;

typedef struct ecs_rule_var_t {
    int8_t kind;           /* variable kind (EcsVarEntity or EcsVarTable)*/
    bool anonymous;        /* variable is anonymous */
    ecs_var_id_t id;       /* variable id */
    ecs_var_id_t table_id; /* id to table variable, if any */
    const char *name;      /* variable name */
#ifdef FLECS_DEBUG
    const char *label;     /* for debugging */
#endif
} ecs_rule_var_t;

/* -- Instruction kinds -- */
typedef enum {
    EcsRuleAnd,            /* And operator: find or match id against variable source */
    EcsRuleAndId,          /* And operator for fixed id (no wildcards/variables) */
    EcsRuleWith,           /* Match id against fixed or variable source */
    EcsRuleAndAny,         /* And operator with support for matching Any src/id */
    EcsRuleTrav,           /* Support for transitive/reflexive queries */
    EcsRuleIdsRight,       /* Find ids in use that match (R, *) wildcard */
    EcsRuleIdsLeft,        /* Find ids in use that match (*, T) wildcard */
    EcsRuleEach,           /* Iterate entities in table, populate entity variable */
    EcsRuleStore,          /* Store table or entity in variable */
    EcsRuleUnion,          /* Combine output of multiple operations */
    EcsRuleEnd,            /* Used to denote end of EcsRuleUnion block */
    EcsRuleNot,            /* Sets iterator state after term was not matched */
    EcsRulePredEq,         /* Test if variable is equal to, or assign to if not set */
    EcsRulePredNeq,        /* Test if variable is not equal to */
    EcsRulePredEqName,     /* Same as EcsRulePredEq but with matching by name */
    EcsRulePredNeqName,    /* Same as EcsRulePredNeq but with matching by name */
    EcsRulePredEqMatch,    /* Same as EcsRulePredEq but with fuzzy matching by name */
    EcsRulePredNeqMatch,   /* Same as EcsRulePredNeq but with fuzzy matching by name */
    EcsRuleSetVars,        /* Populate it.sources from variables */
    EcsRuleSetThis,        /* Populate This entity variable */
    EcsRuleSetFixed,       /* Set fixed source entity ids */
    EcsRuleSetIds,         /* Set fixed (component) ids */
    EcsRuleContain,        /* Test if table contains entity */
    EcsRulePairEq,         /* Test if both elements of pair are the same */
    EcsRuleSetCond,        /* Set conditional value for EcsRuleJmpCondFalse */
    EcsRuleJmpCondFalse,   /* Jump if condition is false */
    EcsRuleJmpNotSet,      /* Jump if variable(s) is not set */
    EcsRuleYield,          /* Yield result back to application */
    EcsRuleNothing         /* Must be last */
} ecs_rule_op_kind_t;

/* Op flags to indicate if ecs_rule_ref_t is entity or variable */
#define EcsRuleIsEntity  (1 << 0)
#define EcsRuleIsVar     (1 << 1)
#define EcsRuleIsSelf    (1 << 6)

/* Op flags used to shift EcsRuleIsEntity and EcsRuleIsVar */
#define EcsRuleSrc     0
#define EcsRuleFirst   2
#define EcsRuleSecond  4

/* References to variable or entity */
typedef union {
    ecs_var_id_t var;
    ecs_entity_t entity;
} ecs_rule_ref_t;

/* Query instruction */
typedef struct ecs_rule_op_t {
    uint8_t kind;              /* Instruction kind */
    ecs_flags8_t flags;        /* Flags storing whether 1st/2nd are variables */
    int8_t field_index;        /* Query field corresponding with operation */
    int8_t term_index;         /* Query term corresponding with operation */
    ecs_rule_lbl_t prev;       /* Backtracking label (no data) */
    ecs_rule_lbl_t next;       /* Forwarding label. Must come after prev */
    ecs_rule_lbl_t other;      /* Misc register used for control flow */
    ecs_flags16_t match_flags; /* Flags that modify matching behavior */
    ecs_rule_ref_t src;
    ecs_rule_ref_t first;
    ecs_rule_ref_t second;
    ecs_flags64_t written;     /* Bitset with variables written by op */
} ecs_rule_op_t;

 /* And context */
typedef struct {
    ecs_id_record_t *idr;
    ecs_table_cache_iter_t it;
    int16_t column;
    int16_t remaining;
} ecs_rule_and_ctx_t;

/* Cache for storing results of downward traversal */
typedef struct {
    ecs_entity_t entity;
    ecs_id_record_t *idr;
    int32_t column;
} ecs_trav_elem_t;

typedef struct {
    ecs_id_t id;
    ecs_id_record_t *idr;
    ecs_vec_t entities;
    bool up;
} ecs_trav_cache_t;

/* Trav context */
typedef struct {
    ecs_rule_and_ctx_t and;
    int32_t index;
    int32_t offset;
    int32_t count;
    ecs_trav_cache_t cache;
    bool yield_reflexive;
} ecs_rule_trav_ctx_t;

 /* Eq context */
typedef struct {
    ecs_table_range_t range;
    int32_t index;
    int16_t name_col;
    bool redo;
} ecs_rule_eq_ctx_t;

 /* Each context */
typedef struct {
    int32_t row;
} ecs_rule_each_ctx_t;

 /* Setthis context */
typedef struct {
    ecs_table_range_t range;
} ecs_rule_setthis_ctx_t;

/* Ids context */
typedef struct {
    ecs_id_record_t *cur;
} ecs_rule_ids_ctx_t;

/* Ctrlflow context (used with Union) */
typedef struct {
    ecs_rule_lbl_t lbl;
} ecs_rule_ctrlflow_ctx_t;

/* Condition context */
typedef struct {
    bool cond;
} ecs_rule_cond_ctx_t;

typedef struct ecs_rule_op_ctx_t {
    union {
        ecs_rule_and_ctx_t and;
        ecs_rule_trav_ctx_t trav;
        ecs_rule_ids_ctx_t ids;
        ecs_rule_eq_ctx_t eq;
        ecs_rule_each_ctx_t each;
        ecs_rule_setthis_ctx_t setthis;
        ecs_rule_ctrlflow_ctx_t ctrlflow;
        ecs_rule_cond_ctx_t cond;
    } is;
} ecs_rule_op_ctx_t;

typedef struct {
    /* Labels used for control flow */
    ecs_rule_lbl_t lbl_union;
    ecs_rule_lbl_t lbl_not;
    ecs_rule_lbl_t lbl_option;
    ecs_rule_lbl_t lbl_cond_eval;
    ecs_rule_lbl_t lbl_or;
    ecs_rule_lbl_t lbl_none;
    ecs_rule_lbl_t lbl_prev; /* If set, use this as default value for prev */
} ecs_rule_compile_ctrlflow_t;

/* Rule compiler state */
typedef struct {
    ecs_vec_t *ops;
    ecs_write_flags_t written; /* Bitmask to check which variables have been written */
    ecs_write_flags_t cond_written; /* Track conditional writes (optional operators) */

    /* Maintain control flow per scope */
    ecs_rule_compile_ctrlflow_t ctrlflow[FLECS_QUERY_SCOPE_NESTING_MAX];
    ecs_rule_compile_ctrlflow_t *cur; /* Current scope */

    int32_t scope; /* Nesting level of query scopes */
    ecs_flags32_t scope_is_not; /* Whether scope is prefixed with not */
} ecs_rule_compile_ctx_t;    

/* Rule run state */
typedef struct {
    uint64_t *written;            /* Bitset to check which variables have been written */
    ecs_rule_lbl_t op_index;      /* Currently evaluated operation */
    ecs_rule_lbl_t prev_index;    /* Previously evaluated operation */
    ecs_rule_lbl_t jump;          /* Set by control flow operations to jump to operation */
    ecs_var_t *vars;              /* Variable storage */
    ecs_iter_t *it;               /* Iterator */
    ecs_rule_op_ctx_t *op_ctx;    /* Operation context (stack) */
    ecs_world_t *world;           /* Reference to world */
    const ecs_rule_t *rule;       /* Reference to rule */
    const ecs_rule_var_t *rule_vars; /* Reference to rule variable array */
} ecs_rule_run_ctx_t;

typedef struct {
    ecs_rule_var_t var;
    const char *name;
} ecs_rule_var_cache_t;

struct ecs_rule_t {
    ecs_header_t hdr;             /* Poly header */
    ecs_filter_t filter;          /* Filter */

    /* Variables */
    ecs_rule_var_t *vars;         /* Variables */
    int32_t var_count;            /* Number of variables */
    int32_t var_pub_count;        /* Number of public variables */
    bool has_table_this;          /* Does rule have [$this] */
    ecs_hashmap_t tvar_index;     /* Name index for table variables */
    ecs_hashmap_t evar_index;     /* Name index for entity variables */
    ecs_rule_var_cache_t vars_cache; /* For trivial rules with only This variables */
    char **var_names;             /* Array with variable names for iterator */
    ecs_var_id_t *src_vars;       /* Array with ids to source variables for fields */

    ecs_rule_op_t *ops;           /* Operations */
    int32_t op_count;             /* Number of operations */

    /* Mixins */
    ecs_iterable_t iterable;
    ecs_poly_dtor_t dtor;

#ifdef FLECS_DEBUG
    int32_t var_size;             /* Used for out of bounds check during compilation */
#endif
};

/* Convert integer to label */
ecs_rule_lbl_t flecs_itolbl(
    int64_t val);

/* Get ref flags (IsEntity) or IsVar) for ref (Src, First, Second) */
ecs_flags16_t flecs_rule_ref_flags(
    ecs_flags16_t flags,
    ecs_flags16_t kind);

/* Check if variable is written */
bool flecs_rule_is_written(
    ecs_var_id_t var_id,
    uint64_t written);

/* Check if ref is written (calls flecs_rule_is_written)*/
bool flecs_ref_is_written(
    const ecs_rule_op_t *op,
    const ecs_rule_ref_t *ref,
    ecs_flags16_t kind,
    uint64_t written);

/* Compile filter to list of operations */
int flecs_rule_compile(
    ecs_world_t *world,
    ecs_stage_t *stage,
    ecs_rule_t *rule);

/* Get allocator from iterator */
ecs_allocator_t* flecs_rule_get_allocator(
    const ecs_iter_t *it);

/* Find all entities when traversing downwards */
void flecs_rule_get_down_cache(
    const ecs_rule_run_ctx_t *ctx,
    ecs_trav_cache_t *cache,
    ecs_entity_t trav,
    ecs_entity_t entity);

/* Find all entities when traversing upwards */
void flecs_rule_get_up_cache(
    const ecs_rule_run_ctx_t *ctx,
    ecs_trav_cache_t *cache,
    ecs_entity_t trav,
    ecs_table_t *table);

/* Free traversal cache */
void flecs_rule_trav_cache_fini(
    ecs_allocator_t *a,
    ecs_trav_cache_t *cache);

#endif


#ifdef FLECS_RULES

#define FlecsRuleOrMarker ((int16_t)-2) /* Marks instruction in OR chain */

static bool flecs_rule_op_is_test[] = {
    [EcsRuleAnd] = true,
    [EcsRuleAndAny] = true,
    [EcsRuleAndId] = true,
    [EcsRuleWith] = true,
    [EcsRuleTrav] = true,
    [EcsRuleContain] = true,
    [EcsRulePairEq] = true,
    [EcsRuleNothing] = false
};

ecs_rule_lbl_t flecs_itolbl(int64_t val) {
    return flecs_ito(int16_t, val);
}

static
ecs_var_id_t flecs_itovar(int64_t val) {
    return flecs_ito(uint8_t, val);
}

static
ecs_var_id_t flecs_utovar(uint64_t val) {
    return flecs_uto(uint8_t, val);
}

#ifdef FLECS_DEBUG
#define flecs_set_var_label(var, lbl) (var)->label = lbl
#else
#define flecs_set_var_label(var, lbl)
#endif

static
bool flecs_rule_is_builtin_pred(
    ecs_term_t *term)
{
    if (term->first.flags & EcsIsEntity) {
        ecs_entity_t id = term->first.id;
        if (id == EcsPredEq || id == EcsPredMatch || id == EcsPredLookup) {
            return true;
        }
    }
    return false;
}

bool flecs_rule_is_written(
    ecs_var_id_t var_id,
    uint64_t written)
{
    if (var_id == EcsVarNone) {
        return true;
    }

    ecs_assert(var_id < EcsRuleMaxVarCount, ECS_INTERNAL_ERROR, NULL);
    return (written & (1ull << var_id)) != 0;
}

static
void flecs_rule_write(
    ecs_var_id_t var_id,
    uint64_t *written)
{
    ecs_assert(var_id < EcsRuleMaxVarCount, ECS_INTERNAL_ERROR, NULL);
    *written |= (1ull << var_id);
}

static
void flecs_rule_write_ctx(
    ecs_var_id_t var_id,
    ecs_rule_compile_ctx_t *ctx,
    bool cond_write)
{
    bool is_written = flecs_rule_is_written(var_id, ctx->written);
    flecs_rule_write(var_id, &ctx->written);
    if (!is_written) {
        if (cond_write) {
            flecs_rule_write(var_id, &ctx->cond_written);
        }
        if (ctx->scope != 0) {
            
        }
    }
}

ecs_flags16_t flecs_rule_ref_flags(
    ecs_flags16_t flags,
    ecs_flags16_t kind)
{
    return (flags >> kind) & (EcsRuleIsVar | EcsRuleIsEntity);
}

bool flecs_ref_is_written(
    const ecs_rule_op_t *op,
    const ecs_rule_ref_t *ref,
    ecs_flags16_t kind,
    uint64_t written)
{
    ecs_flags16_t flags = flecs_rule_ref_flags(op->flags, kind);
    if (flags & EcsRuleIsEntity) {
        ecs_assert(!(flags & EcsRuleIsVar), ECS_INTERNAL_ERROR, NULL);
        if (ref->entity) {
            return true;
        }
    } else if (flags & EcsRuleIsVar) {
        return flecs_rule_is_written(ref->var, written);
    }
    return false;
}

static
bool flecs_rule_var_is_anonymous(
    const ecs_rule_t *rule,
    ecs_var_id_t var_id)
{
    ecs_rule_var_t *var = &rule->vars[var_id];
    return var->anonymous;
}

static
ecs_var_id_t flecs_rule_find_var_id(
    const ecs_rule_t *rule,
    const char *name,
    ecs_var_kind_t kind)
{
    ecs_assert(name != NULL, ECS_INTERNAL_ERROR, NULL);

    /* Backwards compatibility */
    if (!ecs_os_strcmp(name, "This")) {
        name = "this";
    }

    if (kind == EcsVarTable) {
        if (!ecs_os_strcmp(name, EcsThisName)) {
            if (rule->has_table_this) {
                return 0;
            } else {
                return EcsVarNone;
            }
        }

        if (!flecs_name_index_is_init(&rule->tvar_index)) {
            return EcsVarNone;
        }

        uint64_t index = flecs_name_index_find(
            &rule->tvar_index, name, 0, 0);
        if (index == 0) {
            return EcsVarNone;
        }
        return flecs_utovar(index);
    }

    if (kind == EcsVarEntity) {
        if (!flecs_name_index_is_init(&rule->evar_index)) {
            return EcsVarNone;
        }

        uint64_t index = flecs_name_index_find(
            &rule->evar_index, name, 0, 0);
        if (index == 0) {
            return EcsVarNone;
        }
        return flecs_utovar(index);
    }

    ecs_assert(kind == EcsVarAny, ECS_INTERNAL_ERROR, NULL);

    /* If searching for any kind of variable, start with most specific */
    ecs_var_id_t index = flecs_rule_find_var_id(rule, name, EcsVarEntity);
    if (index != EcsVarNone) {
        return index;
    }

    return flecs_rule_find_var_id(rule, name, EcsVarTable);
}

int32_t ecs_rule_var_count(
    const ecs_rule_t *rule)
{
    return rule->var_pub_count;
}

int32_t ecs_rule_find_var(
    const ecs_rule_t *rule,
    const char *name)
{
    ecs_var_id_t var_id = flecs_rule_find_var_id(rule, name, EcsVarEntity);
    if (var_id == EcsVarNone) {
        if (rule->filter.flags & EcsFilterMatchThis) {
            if (!ecs_os_strcmp(name, "This")) {
                name = "this";
            }
            if (!ecs_os_strcmp(name, EcsThisName)) {
                var_id = 0;
            }
        }
        if (var_id == EcsVarNone) {
            return -1;
        }
    }
    return (int32_t)var_id;
}

const char* ecs_rule_var_name(
    const ecs_rule_t *rule,
    int32_t var_id)
{
    if (var_id) {
        return rule->vars[var_id].name;
    } else {
        return EcsThisName;
    }
}

bool ecs_rule_var_is_entity(
    const ecs_rule_t *rule,
    int32_t var_id)
{
    return rule->vars[var_id].kind == EcsVarEntity;
}

static
const char* flecs_term_id_var_name(
    ecs_term_id_t *term_id)
{
    if (!(term_id->flags & EcsIsVariable)) {
        return NULL;
    }

    if (term_id->id == EcsThis) {
        return EcsThisName;
    }

    return term_id->name;
}

static
bool flecs_term_id_is_wildcard(
    ecs_term_id_t *term_id)
{
    if ((term_id->flags & EcsIsVariable) && 
        ((term_id->id == EcsWildcard) || (term_id->id == EcsAny))) 
    {
        return true;
    }
    return false;
}

static
ecs_var_id_t flecs_rule_add_var(
    ecs_rule_t *rule,
    const char *name,
    ecs_vec_t *vars,
    ecs_var_kind_t kind)
{
    ecs_hashmap_t *var_index = NULL;
    ecs_var_id_t var_id = EcsVarNone;
    if (name) {
        if (kind == EcsVarAny) {
            var_id = flecs_rule_find_var_id(rule, name, EcsVarEntity);
            if (var_id != EcsVarNone) {
                return var_id;
            }

            var_id = flecs_rule_find_var_id(rule, name, EcsVarTable);
            if (var_id != EcsVarNone) {
                return var_id;
            }

            kind = EcsVarTable;
        } else {
            var_id = flecs_rule_find_var_id(rule, name, kind);
            if (var_id != EcsVarNone) {
                return var_id;
            }
        }

        if (kind == EcsVarTable) {
            var_index = &rule->tvar_index;
        } else {
            var_index = &rule->evar_index;
        }

        /* If we're creating an entity var, check if it has a table variant */
        if (kind == EcsVarEntity && var_id == EcsVarNone) {
            var_id = flecs_rule_find_var_id(rule, name, EcsVarTable);
        }
    }

    ecs_rule_var_t *var;
    if (vars) {
        var = ecs_vec_append_t(NULL, vars, ecs_rule_var_t);
        var->id = flecs_itovar(ecs_vec_count(vars));
    } else {
        ecs_dbg_assert(rule->var_count < rule->var_size, ECS_INTERNAL_ERROR, NULL);
        var = &rule->vars[rule->var_count];
        var->id = flecs_itovar(rule->var_count);
        rule->var_count ++;
    }

    var->kind = flecs_ito(int8_t, kind);
    var->name = name;
    var->table_id = var_id;
    flecs_set_var_label(var, NULL);

    if (name) {
        flecs_name_index_init_if(var_index, NULL);
        flecs_name_index_ensure(var_index, var->id, name, 0, 0);
        var->anonymous = name ? name[0] == '_' : false;
    }

    return var->id;
}

static
ecs_var_id_t flecs_rule_add_var_for_term_id(
    ecs_rule_t *rule,
    ecs_term_id_t *term_id,
    ecs_vec_t *vars,
    ecs_var_kind_t kind)
{
    const char *name = flecs_term_id_var_name(term_id);
    if (!name) {
        return EcsVarNone;
    }

    return flecs_rule_add_var(rule, name, vars, kind);
}

static
void flecs_rule_discover_vars(
    ecs_stage_t *stage,
    ecs_rule_t *rule)
{
    ecs_vec_t *vars = &stage->variables; /* Buffer to reduce allocs */
    ecs_vec_reset_t(NULL, vars, ecs_rule_var_t);

    ecs_term_t *terms = rule->filter.terms;
    int32_t i, anonymous_count = 0, count = rule->filter.term_count;
    int32_t anonymous_table_count = 0, scope = 0, scoped_var_index = 0;
    bool table_this = false, entity_before_table_this = false;

    /* For This table lookups during discovery. This will be overwritten after
     * discovery with whether the rule actually has a This table variable. */
    rule->has_table_this = true;

    for (i = 0; i < count; i ++) {
        ecs_term_t *term = &terms[i];
        ecs_term_id_t *first = &term->first;
        ecs_term_id_t *second = &term->second;
        ecs_term_id_t *src = &term->src;

        if (first->id == EcsScopeOpen) {
            /* Keep track of which variables are first used in scope, so that we
             * can mark them as anonymous. Terms inside a scope are collapsed 
             * into a single result, which means that outside of the scope the
             * value of those variables is undefined. */
            if (!scope) {
                scoped_var_index = ecs_vec_count(vars);
            }
            scope ++;
            continue;
        } else if (first->id == EcsScopeClose) {
            if (!--scope) {
                /* Any new variables declared after entering a scope should be
                 * marked as anonymous. */
                int32_t v;
                for (v = scoped_var_index; v < ecs_vec_count(vars); v ++) {
                    ecs_vec_get_t(vars, ecs_rule_var_t, v)->anonymous = true;
                }
            }
            continue;
        }

        ecs_var_id_t first_var_id = flecs_rule_add_var_for_term_id(
            rule, first, vars, EcsVarEntity);
        if (first_var_id == EcsVarNone) {
            /* If first is not a variable, check if we need to insert anonymous
             * variable for resolving component inheritance */
            if (term->flags & EcsTermIdInherited) {
                anonymous_count += 2; /* table & entity variable */
            }

            /* If first is a wildcard, insert anonymous variable */
            if (flecs_term_id_is_wildcard(first)) {
                anonymous_count ++;
            }
        }

        if ((src->flags & EcsIsVariable) && (src->id != EcsThis)) {
            const char *var_name = flecs_term_id_var_name(src);
            if (var_name) {
                ecs_var_id_t var_id = flecs_rule_find_var_id(
                    rule, var_name, EcsVarEntity);
                if (var_id == EcsVarNone || var_id == first_var_id) {
                    var_id = flecs_rule_add_var(
                        rule, var_name, vars, EcsVarEntity);

                    /* Mark variable as one for which we need to create a table
                     * variable. Don't create table variable now, so that we can
                     * store it in the non-public part of the variable array. */
                    ecs_rule_var_t *var = ecs_vec_get_t(
                        vars, ecs_rule_var_t, (int32_t)var_id - 1);
                    ecs_assert(var != NULL, ECS_INTERNAL_ERROR, NULL);
                    var->kind = EcsVarAny;

                    anonymous_table_count ++;
                }

                if (var_id != EcsVarNone) {
                    /* Track of which variable ids are used as field source */
                    if (!rule->src_vars) {
                        rule->src_vars = ecs_os_calloc_n(ecs_var_id_t,
                            rule->filter.field_count);
                    }

                    rule->src_vars[term->field_index] = var_id;
                }
            } else {
                if (flecs_term_id_is_wildcard(src)) {
                    anonymous_count ++;
                }
            }
        } else if ((src->flags & EcsIsVariable) && (src->id == EcsThis)) {
            if (flecs_rule_is_builtin_pred(term) && term->oper == EcsOr) {
                flecs_rule_add_var(rule, EcsThisName, vars, EcsVarEntity);
            }
        }

        if (flecs_rule_add_var_for_term_id(
            rule, second, vars, EcsVarEntity) == EcsVarNone)
        {
            /* If second is a wildcard, insert anonymous variable */
            if (flecs_term_id_is_wildcard(second)) {
                anonymous_count ++;
            }
        }

        if (src->flags & EcsIsVariable && second->flags & EcsIsVariable) {
            if (term->flags & EcsTermTransitive) {
                /* Anonymous variable to store temporary id for finding 
                 * targets for transitive relationship, see compile_term. */
                anonymous_count ++;
            }
        }

        /* Track if a This entity variable is used before a potential This table 
         * variable. If this happens, the rule has no This table variable */
        if (src->id == EcsThis) {
            table_this = true;
        }
        if (first->id == EcsThis || second->id == EcsThis) {
            if (!table_this) {
                entity_before_table_this = true;
            }
        }
    }

    int32_t var_count = ecs_vec_count(vars);

    /* Add non-This table variables */
    if (anonymous_table_count) {
        anonymous_table_count = 0;
        for (i = 0; i < var_count; i ++) {
            ecs_rule_var_t *var = ecs_vec_get_t(vars, ecs_rule_var_t, i);
            if (var->kind == EcsVarAny) {
                var->kind = EcsVarEntity;

                ecs_var_id_t var_id = flecs_rule_add_var(
                    rule, var->name, vars, EcsVarTable);
                ecs_vec_get_t(vars, ecs_rule_var_t, i)->table_id = var_id;
                anonymous_table_count ++;
            }
        }

        var_count = ecs_vec_count(vars);
    }

    /* Always include spot for This variable, even if rule doesn't use it */
    var_count ++;

    ecs_rule_var_t *rule_vars = &rule->vars_cache.var;
    if ((var_count + anonymous_count) > 1) {
        rule_vars = ecs_os_malloc(
            (ECS_SIZEOF(ecs_rule_var_t) + ECS_SIZEOF(char*)) * 
                (var_count + anonymous_count));
    }

    rule->vars = rule_vars;
    rule->var_count = var_count;
    rule->var_pub_count = var_count;
    rule->has_table_this = !entity_before_table_this;

#ifdef FLECS_DEBUG
    rule->var_size = var_count + anonymous_count;
#endif

    char **var_names = ECS_ELEM(rule_vars, ECS_SIZEOF(ecs_rule_var_t), 
        var_count + anonymous_count);
    rule->var_names = (char**)var_names;

    rule_vars[0].kind = EcsVarTable;
    rule_vars[0].name = NULL;
    flecs_set_var_label(&rule_vars[0], NULL);
    rule_vars[0].id = 0;
    rule_vars[0].table_id = EcsVarNone;
    var_names[0] = (char*)rule_vars[0].name;
    rule_vars ++;
    var_names ++;
    var_count --;

    if (var_count) {
        ecs_rule_var_t *user_vars = ecs_vec_first_t(vars, ecs_rule_var_t);
        ecs_os_memcpy_n(rule_vars, user_vars, ecs_rule_var_t, var_count);
        for (i = 0; i < var_count; i ++) {
            var_names[i] = (char*)rule_vars[i].name;
        }
    }

    /* Hide anonymous table variables from application */
    rule->var_pub_count -= anonymous_table_count;
}

static
ecs_var_id_t flecs_rule_most_specific_var(
    ecs_rule_t *rule,
    const char *name,
    ecs_var_kind_t kind,
    ecs_rule_compile_ctx_t *ctx)
{
    if (kind == EcsVarTable || kind == EcsVarEntity) {
        return flecs_rule_find_var_id(rule, name, kind);
    }

    ecs_var_id_t tvar = flecs_rule_find_var_id(rule, name, EcsVarTable);
    if ((tvar != EcsVarNone) && !flecs_rule_is_written(tvar, ctx->written)) {
        /* If variable of any kind is requested and variable hasn't been written
         * yet, write to table variable */
        return tvar;
    }

    ecs_var_id_t evar = flecs_rule_find_var_id(rule, name, EcsVarEntity);
    if ((evar != EcsVarNone) && flecs_rule_is_written(evar, ctx->written)) {
        /* If entity variable is available and written to, it contains the most
         * specific result and should be used. */
        return evar;
    }

    /* If table var is written, and entity var doesn't exist or is not written,
     * return table var */
    ecs_assert(tvar != EcsVarNone, ECS_INTERNAL_ERROR, NULL);
    return tvar;
}

static
ecs_rule_lbl_t flecs_rule_op_insert(
    ecs_rule_op_t *op,
    ecs_rule_compile_ctx_t *ctx)
{
    ecs_rule_op_t *elem = ecs_vec_append_t(NULL, ctx->ops, ecs_rule_op_t);
    int32_t count = ecs_vec_count(ctx->ops);
    *elem = *op;
    if (count > 1) {
        if (ctx->cur->lbl_union == -1) {
            /* Variables written by previous instruction can't be written by
             * this instruction, except when this is a union. */
            elem->written &= ~elem[-1].written;
        }
    }

    if (ctx->cur->lbl_union != -1) {
        elem->prev = ctx->cur->lbl_union;
    } else if (ctx->cur->lbl_prev != -1) {
        elem->prev = ctx->cur->lbl_prev;
        ctx->cur->lbl_prev = -1;
    } else {
        elem->prev = flecs_itolbl(count - 2);
    }

    elem->next = flecs_itolbl(count);
    return flecs_itolbl(count - 1);
}

static
int32_t flecs_rule_not_insert(
    ecs_rule_op_t *op,
    ecs_rule_compile_ctx_t *ctx)
{
    ecs_rule_op_t *op_last = ecs_vec_last_t(ctx->ops, ecs_rule_op_t);
    if (op_last && op_last->kind == EcsRuleNot) {
        /* There can be multiple reasons for inserting a Not operation, ensure
         * that only one is created. */
        ecs_assert(op_last->field_index == op->field_index, 
            ECS_INTERNAL_ERROR, NULL);
        return ecs_vec_count(ctx->ops) - 1;
    }

    ecs_rule_op_t not_op = {0};
    not_op.kind = EcsRuleNot;
    not_op.field_index = op->field_index;
    not_op.first = op->first;
    not_op.second = op->second;
    not_op.flags = op->flags;
    not_op.flags &= (ecs_flags8_t)~(EcsRuleIsVar << EcsRuleSrc);
    if (op->flags & (EcsRuleIsEntity << EcsRuleSrc)) {
        not_op.src.entity = op->src.entity;
    }

    return flecs_rule_op_insert(&not_op, ctx);
}

static
void flecs_rule_begin_none(
    ecs_rule_compile_ctx_t *ctx)
{
    ctx->cur->lbl_none = flecs_itolbl(ecs_vec_count(ctx->ops));

    ecs_rule_op_t jmp = {0};
    jmp.kind = EcsRuleJmpCondFalse;
    flecs_rule_op_insert(&jmp, ctx);
}

static
void flecs_rule_begin_not(
    ecs_rule_compile_ctx_t *ctx)
{
    ctx->cur->lbl_not = flecs_itolbl(ecs_vec_count(ctx->ops));
}

static
void flecs_rule_end_not(
    ecs_rule_op_t *op,
    ecs_rule_compile_ctx_t *ctx,
    bool update_labels)
{
    if (ctx->cur->lbl_none != -1) {
        ecs_rule_op_t setcond = {0};
        setcond.kind = EcsRuleSetCond;
        setcond.other = ctx->cur->lbl_none;
        flecs_rule_op_insert(&setcond, ctx);
    }

    flecs_rule_not_insert(op, ctx);

    ecs_rule_op_t *ops = ecs_vec_first_t(ctx->ops, ecs_rule_op_t);
    int32_t i, count = ecs_vec_count(ctx->ops);
    if (update_labels) {
        for (i = ctx->cur->lbl_not; i < count; i ++) {
            ecs_rule_op_t *cur = &ops[i];
            if (flecs_rule_op_is_test[cur->kind]) {
                cur->prev = flecs_itolbl(count - 1);
                if (i == (count - 2)) {
                    cur->next = flecs_itolbl(ctx->cur->lbl_not - 1);
                }
            }
        }
    }

    /* After a match was found, return to op before Not operation */
    ecs_rule_op_t *not_ptr = ecs_vec_last_t(ctx->ops, ecs_rule_op_t);
    not_ptr->prev = flecs_itolbl(ctx->cur->lbl_not - 1);

    if (ctx->cur->lbl_none != -1) {
        /* setcond */
        ops[count - 2].next = flecs_itolbl(ctx->cur->lbl_none - 1);
        /* last actual instruction */
        if (update_labels) {
            ops[count - 3].prev = flecs_itolbl(count - 4);
        }
        /* jfalse */
        ops[ctx->cur->lbl_none].other =
            flecs_itolbl(count - 1); /* jump to not */
        /* not */
        ops[count - 1].prev = flecs_itolbl(ctx->cur->lbl_none - 1);
    }

    ctx->cur->lbl_not = -1;
    ctx->cur->lbl_none = -1;
}

static
void flecs_rule_begin_option(
    ecs_rule_compile_ctx_t *ctx)
{
    ctx->cur->lbl_option = flecs_itolbl(ecs_vec_count(ctx->ops));

    {
        ecs_rule_op_t new_op = {0};
        new_op.kind = EcsRuleJmpCondFalse;
        flecs_rule_op_insert(&new_op, ctx);
    }
}

static
void flecs_rule_end_option(
    ecs_rule_op_t *op,
    ecs_rule_compile_ctx_t *ctx)
{
    flecs_rule_not_insert(op, ctx);
    ecs_rule_op_t *ops = ecs_vec_first_t(ctx->ops, ecs_rule_op_t);
    int32_t count = ecs_vec_count(ctx->ops);

    ops[ctx->cur->lbl_option].other = flecs_itolbl(count - 1);
    ops[count - 2].next = flecs_itolbl(count);

    ecs_rule_op_t new_op = {0};
    new_op.kind = EcsRuleSetCond;
    new_op.other = ctx->cur->lbl_option;
    flecs_rule_op_insert(&new_op, ctx);

    ctx->cur->lbl_option = -1;
}

static
void flecs_rule_begin_cond_eval(
    ecs_rule_op_t *op,
    ecs_rule_compile_ctx_t *ctx,
    ecs_write_flags_t cond_write_state)
{
    ecs_var_id_t first_var = EcsVarNone, second_var = EcsVarNone, src_var = EcsVarNone;
    ecs_write_flags_t cond_mask = 0;

    if (flecs_rule_ref_flags(op->flags, EcsRuleFirst) == EcsRuleIsVar) {
        first_var = op->first.var;
        cond_mask |= (1ull << first_var);
    }
    if (flecs_rule_ref_flags(op->flags, EcsRuleSecond) == EcsRuleIsVar) {
        second_var = op->second.var;
        cond_mask |= (1ull << second_var);
    }
    if (flecs_rule_ref_flags(op->flags, EcsRuleSrc) == EcsRuleIsVar) {
        src_var = op->src.var;
        cond_mask |= (1ull << src_var);
    }

    /* If this term uses conditionally set variables, insert instruction that
     * jumps over the term if the variables weren't set yet. */
    if (cond_mask & cond_write_state) {
        ctx->cur->lbl_cond_eval = flecs_itolbl(ecs_vec_count(ctx->ops));

        ecs_rule_op_t jmp_op = {0};
        jmp_op.kind = EcsRuleJmpNotSet;

        if ((first_var != EcsVarNone) && cond_write_state & (1ull << first_var)) {
            jmp_op.flags |= (EcsRuleIsVar << EcsRuleFirst);
            jmp_op.first.var = first_var;
        }
        if ((second_var != EcsVarNone) && cond_write_state & (1ull << second_var)) {
            jmp_op.flags |= (EcsRuleIsVar << EcsRuleSecond);
            jmp_op.second.var = second_var;
        }
        if ((src_var != EcsVarNone) && cond_write_state & (1ull << src_var)) {
            jmp_op.flags |= (EcsRuleIsVar << EcsRuleSrc);
            jmp_op.src.var = src_var;
        }

        flecs_rule_op_insert(&jmp_op, ctx);
    } else {
        ctx->cur->lbl_cond_eval = -1;
    }
}

static
void flecs_rule_end_cond_eval(
    ecs_rule_op_t *op,
    ecs_rule_compile_ctx_t *ctx)
{
    if (ctx->cur->lbl_cond_eval == -1) {
        return;
    }

    flecs_rule_not_insert(op, ctx);

    ecs_rule_op_t *ops = ecs_vec_first_t(ctx->ops, ecs_rule_op_t);
    int32_t count = ecs_vec_count(ctx->ops);
    ops[ctx->cur->lbl_cond_eval].other = flecs_itolbl(count - 1);
    ops[count - 2].next = flecs_itolbl(count);
}

static
void flecs_rule_next_or(
    ecs_rule_compile_ctx_t *ctx)
{
    ecs_rule_op_t *ops = ecs_vec_first_t(ctx->ops, ecs_rule_op_t);
    int32_t count = ecs_vec_count(ctx->ops);
    ops[count - 1].next = FlecsRuleOrMarker;
}

static
void flecs_rule_begin_or(
    ecs_rule_compile_ctx_t *ctx)
{
    ctx->cur->lbl_or = flecs_itolbl(ecs_vec_count(ctx->ops) - 1);
    flecs_rule_next_or(ctx);
}

static
void flecs_rule_end_or(
    ecs_rule_compile_ctx_t *ctx)
{
    flecs_rule_next_or(ctx);

    ecs_rule_op_t *ops = ecs_vec_first_t(ctx->ops, ecs_rule_op_t);
    int32_t i, count = ecs_vec_count(ctx->ops);
    int32_t prev_or = -2;
    for (i = ctx->cur->lbl_or; i < count; i ++) {
        if (ops[i].next == FlecsRuleOrMarker) {
            if (prev_or != -2) {
                ops[prev_or].prev = flecs_itolbl(i);
            }
            ops[i].next = flecs_itolbl(count);
            prev_or = i;
        }
    }

    ops[count - 1].prev = flecs_itolbl(ctx->cur->lbl_or - 1);

    /* Set prev of next instruction to before the start of the OR chain */
    ctx->cur->lbl_prev = flecs_itolbl(ctx->cur->lbl_or - 1);
    ctx->cur->lbl_or = -1;
}

static
void flecs_rule_begin_union(
    ecs_rule_compile_ctx_t *ctx)
{
    ecs_rule_op_t op = {0};
    op.kind = EcsRuleUnion;
    ctx->cur->lbl_union = flecs_rule_op_insert(&op, ctx);
}

static
void flecs_rule_end_union(
    ecs_rule_compile_ctx_t *ctx)
{
    flecs_rule_next_or(ctx);

    ecs_rule_op_t op = {0};
    op.kind = EcsRuleEnd;
    ctx->cur->lbl_union = -1;
    ecs_rule_lbl_t next = flecs_rule_op_insert(&op, ctx);
    
    ecs_rule_op_t *ops = ecs_vec_first_t(ctx->ops, ecs_rule_op_t);
    int32_t i = ecs_vec_count(ctx->ops) - 2;
    for (; i >= 0 && (ops[i].kind != EcsRuleUnion); i --) {
        if (ops[i].next == FlecsRuleOrMarker) {
            ops[i].next = next;
        }
    }

    ops[next].prev = flecs_itolbl(i);
    ops[i].next = next;
}

static
void flecs_rule_insert_each(
    ecs_var_id_t tvar,
    ecs_var_id_t evar,
    ecs_rule_compile_ctx_t *ctx,
    bool cond_write)
{
    ecs_rule_op_t each = {0};
    each.kind = EcsRuleEach;
    each.src.var = evar;
    each.first.var = tvar;
    each.flags = (EcsRuleIsVar << EcsRuleSrc) | 
                 (EcsRuleIsVar << EcsRuleFirst);
    flecs_rule_write_ctx(evar, ctx, cond_write);
    flecs_rule_write(evar, &each.written);
    flecs_rule_op_insert(&each, ctx);
}

static
void flecs_rule_insert_unconstrained_transitive(
    ecs_rule_t *rule,
    ecs_rule_op_t *op,
    ecs_rule_compile_ctx_t *ctx,
    bool cond_write)
{
    /* Create anonymous variable to store the target ids. This will return the
     * list of targets without constraining the variable of the term, which
     * needs to stay variable to find all transitive relationships for a src. */
    ecs_var_id_t tgt = flecs_rule_add_var(rule, NULL, NULL, EcsVarEntity);
    flecs_set_var_label(&rule->vars[tgt], rule->vars[op->second.var].name);

    /* First, find ids to start traversal from. This fixes op.second. */
    ecs_rule_op_t find_ids = {0};
    find_ids.kind = EcsRuleIdsRight;
    find_ids.field_index = -1;
    find_ids.first = op->first;
    find_ids.second = op->second;
    find_ids.flags = op->flags;
    find_ids.flags &= (ecs_flags8_t)~((EcsRuleIsVar|EcsRuleIsEntity) << EcsRuleSrc);
    find_ids.second.var = tgt;
    flecs_rule_write_ctx(tgt, ctx, cond_write);
    flecs_rule_write(tgt, &find_ids.written);
    flecs_rule_op_insert(&find_ids, ctx);

    /* Next, iterate all tables for the ids. This fixes op.src */
    ecs_rule_op_t and_op = {0};
    and_op.kind = EcsRuleAnd;
    and_op.field_index = op->field_index;
    and_op.first = op->first;
    and_op.second = op->second;
    and_op.src = op->src;
    and_op.flags = op->flags | EcsRuleIsSelf;
    and_op.second.var = tgt;
    flecs_rule_write_ctx(and_op.src.var, ctx, cond_write);
    flecs_rule_write(and_op.src.var, &and_op.written);
    flecs_rule_op_insert(&and_op, ctx);
}

static
void flecs_rule_insert_inheritance(
    ecs_rule_t *rule,
    ecs_term_t *term,
    ecs_rule_op_t *op,
    ecs_rule_compile_ctx_t *ctx,
    bool cond_write)
{
    /* Anonymous variable to store the resolved component ids */
    ecs_var_id_t tvar = flecs_rule_add_var(rule, NULL, NULL, EcsVarTable);
    ecs_var_id_t evar = flecs_rule_add_var(rule, NULL, NULL, EcsVarEntity);
    flecs_set_var_label(&rule->vars[tvar], ecs_get_name(rule->filter.world, term->first.id));
    flecs_set_var_label(&rule->vars[evar], ecs_get_name(rule->filter.world, term->first.id));

    ecs_rule_op_t trav_op = {0};
    trav_op.kind = EcsRuleTrav;
    trav_op.field_index = -1;
    trav_op.first.entity = term->first.trav;
    trav_op.second.entity = term->first.id;
    trav_op.src.var = tvar;
    trav_op.flags = EcsRuleIsSelf;
    trav_op.flags |= (EcsRuleIsEntity << EcsRuleFirst);
    trav_op.flags |= (EcsRuleIsEntity << EcsRuleSecond);
    trav_op.flags |= (EcsRuleIsVar << EcsRuleSrc);
    trav_op.written |= (1ull << tvar);
    if (term->first.flags & EcsSelf) {
        trav_op.match_flags |= EcsTermReflexive;
    }
    flecs_rule_op_insert(&trav_op, ctx);
    flecs_rule_insert_each(tvar, evar, ctx, cond_write);

    ecs_rule_ref_t r = { .var = evar };
    op->first = r;
    op->flags &= (ecs_flags8_t)~(EcsRuleIsEntity << EcsRuleFirst);
    op->flags |= (EcsRuleIsVar << EcsRuleFirst);
}

static
void flecs_rule_compile_term_id(
    ecs_world_t *world,
    ecs_rule_t *rule,
    ecs_rule_op_t *op,
    ecs_term_id_t *term_id,
    ecs_rule_ref_t *ref,
    ecs_flags8_t ref_kind,
    ecs_var_kind_t kind,
    ecs_rule_compile_ctx_t *ctx)
{
    (void)world;

    if (!ecs_term_id_is_set(term_id)) {
        return;
    }

    if (term_id->flags & EcsIsVariable) {
        op->flags |= (ecs_flags8_t)(EcsRuleIsVar << ref_kind);
        const char *name = flecs_term_id_var_name(term_id);
        if (name) {
            ref->var = flecs_rule_most_specific_var(rule, name, kind, ctx);
        } else {
            bool is_wildcard = flecs_term_id_is_wildcard(term_id);
            if (is_wildcard && (kind == EcsVarAny)) {
                ref->var = flecs_rule_add_var(rule, NULL, NULL, EcsVarTable);
            } else {
                ref->var = flecs_rule_add_var(rule, NULL, NULL, EcsVarEntity);
            }
            if (is_wildcard) {
                flecs_set_var_label(&rule->vars[ref->var], 
                    ecs_get_name(world, term_id->id));
            }
        }
        ecs_assert(ref->var != EcsVarNone, ECS_INTERNAL_ERROR, NULL);
    }

    if (term_id->flags & EcsIsEntity) {
        op->flags |= (ecs_flags8_t)(EcsRuleIsEntity << ref_kind);
        ref->entity = term_id->id;
    }
}

static
bool flecs_rule_compile_ensure_vars(
    ecs_rule_t *rule,
    ecs_rule_op_t *op,
    ecs_rule_ref_t *ref,
    ecs_flags16_t ref_kind,
    ecs_rule_compile_ctx_t *ctx,
    bool cond_write)
{
    ecs_flags16_t flags = flecs_rule_ref_flags(op->flags, ref_kind);
    bool written = false;

    if (flags & EcsRuleIsVar) {
        ecs_var_id_t var_id = ref->var;
        ecs_rule_var_t *var = &rule->vars[var_id];
        if (var->kind == EcsVarEntity && !flecs_rule_is_written(var_id, ctx->written)) {
            /* If entity variable is not yet written but a table variant exists
             * that has been written, insert each operation to translate from
             * entity variable to table */
            ecs_var_id_t tvar = var->table_id;
            if ((tvar != EcsVarNone) && flecs_rule_is_written(tvar, ctx->written)) {
                flecs_rule_insert_each(tvar, var_id, ctx, cond_write);

                /* Variable was written, just not as entity */
                written = true;
            }
        }

        written |= flecs_rule_is_written(var_id, ctx->written);

        /* After evaluating a term, a used variable is always written */
        flecs_rule_write(var_id, &op->written);
        flecs_rule_write_ctx(var_id, ctx, cond_write);
    } else {
        /* If it's not a variable, it's always written */
        written = true;
    }

    return written;
}

static
void flecs_rule_insert_contains(
    ecs_rule_t *rule,
    ecs_var_id_t src_var,
    ecs_var_id_t other_var,
    ecs_rule_compile_ctx_t *ctx)
{
    ecs_rule_op_t contains = {0};
    if ((src_var != other_var) && (src_var == rule->vars[other_var].table_id)) {
        contains.kind = EcsRuleContain;
        contains.src.var = src_var;
        contains.first.var = other_var;
        contains.flags |=(EcsRuleIsVar << EcsRuleSrc) | 
                            (EcsRuleIsVar << EcsRuleFirst);
        flecs_rule_op_insert(&contains, ctx);
    }
}

static
void flecs_rule_insert_pair_eq(
    int32_t field_index,
    ecs_rule_compile_ctx_t *ctx)
{
    ecs_rule_op_t contains = {0};
    contains.kind = EcsRulePairEq;
    contains.field_index = flecs_ito(int8_t, field_index);
    flecs_rule_op_insert(&contains, ctx);
}

static
bool flecs_rule_term_fixed_id(
    ecs_filter_t *filter,
    ecs_term_t *term)
{
    /* Transitive/inherited terms have variable ids */
    if (term->flags & (EcsTermTransitive|EcsTermIdInherited)) {
        return false;
    }

    /* Or terms can match different ids */
    if (term->oper == EcsOr) {
        return false;
    }
    if ((term != filter->terms) && term[-1].oper == EcsOr) {
        return false;
    }

    /* Wildcards can assume different ids */
    if (ecs_id_is_wildcard(term->id)) {
        return false;
    }

    /* Any terms can have fixed ids, but they require special handling */
    if (term->flags & (EcsTermMatchAny|EcsTermMatchAnySrc)) {
        return false;
    }

    /* First terms that are Not or Optional require special handling */
    if (term->oper == EcsNot || term->oper == EcsOptional) {
        if (term == filter->terms) {
            return false;
        }
    }

    return true;
}

static
int flecs_rule_compile_builtin_pred(
    ecs_term_t *term,
    ecs_rule_op_t *op,
    ecs_write_flags_t write_state)
{
    ecs_entity_t id = term->first.id;

    ecs_rule_op_kind_t eq[] = {EcsRulePredEq, EcsRulePredNeq};
    ecs_rule_op_kind_t eq_name[] = {EcsRulePredEqName, EcsRulePredNeqName};
    ecs_rule_op_kind_t eq_match[] = {EcsRulePredEqMatch, EcsRulePredNeqMatch};
    
    ecs_flags16_t flags_2nd = flecs_rule_ref_flags(op->flags, EcsRuleSecond);

    if (id == EcsPredEq) {
        if (term->second.flags & EcsIsName) {
            op->kind = flecs_ito(uint8_t, eq_name[term->oper == EcsNot]);
        } else {
            op->kind = flecs_ito(uint8_t, eq[term->oper == EcsNot]);
        }
    } else if (id == EcsPredMatch) {
        op->kind = flecs_ito(uint8_t, eq_match[term->oper == EcsNot]);
    }

    op->first = op->src;
    op->src = (ecs_rule_ref_t){0};
    op->flags &= (ecs_flags8_t)~((EcsRuleIsEntity|EcsRuleIsVar) << EcsRuleSrc);
    op->flags &= (ecs_flags8_t)~((EcsRuleIsEntity|EcsRuleIsVar) << EcsRuleFirst);
    op->flags |= EcsRuleIsVar << EcsRuleFirst;

    if (flags_2nd & EcsRuleIsVar) {
        if (!(write_state & (1ull << op->second.var))) {
            ecs_err("uninitialized variable '%s' on right-hand side of "
                "equality operator", term->second.name);
            return -1;
        }
    }

    return 0;
}

static
void flecs_rule_compile_push(
    ecs_rule_compile_ctx_t *ctx)
{
    ctx->cur = &ctx->ctrlflow[++ ctx->scope];
    ctx->cur->lbl_union = -1;
    ctx->cur->lbl_prev = -1;
    ctx->cur->lbl_not = -1;
    ctx->cur->lbl_none = -1;
}

static
void flecs_rule_compile_pop(
    ecs_rule_compile_ctx_t *ctx)
{
    ctx->cur = &ctx->ctrlflow[-- ctx->scope];
}

static
int flecs_rule_compile_term(
    ecs_world_t *world,
    ecs_rule_t *rule,
    ecs_term_t *term,
    ecs_rule_compile_ctx_t *ctx)
{
    bool first_term = term == rule->filter.terms;
    bool first_is_var = term->first.flags & EcsIsVariable;
    bool second_is_var = term->second.flags & EcsIsVariable;
    bool src_is_var = term->src.flags & EcsIsVariable;
    bool cond_write = term->oper == EcsOptional;
    bool builtin_pred = flecs_rule_is_builtin_pred(term);
    bool is_not = (term->oper == EcsNot) && !builtin_pred;
    ecs_rule_op_t op = {0};

    if (!term->src.id && term->src.flags & EcsIsEntity) {
        /* If the term has a 0 source, check if it's a scope open/close */
        if (term->first.id == EcsScopeOpen) {
            if (term->oper == EcsNot) {
                ctx->scope_is_not |= (ecs_flags32_t)(1ull << ctx->scope);
            } else {
                ctx->scope_is_not &= (ecs_flags32_t)~(1ull << ctx->scope);
            }
        } else if (term->first.id == EcsScopeClose) {
            flecs_rule_compile_pop(ctx);
            if (ctx->scope_is_not & (ecs_flags32_t)(1ull << ctx->scope)) {
                op.field_index = -1;
                flecs_rule_end_not(&op, ctx, false);
            }
        } else {
            /* Nothing to be done */
        }
        return 0;
    }

    /* Default instruction for And operators. If the source is fixed (like for
     * singletons or terms with an entity source), use With, which like And but
     * just matches against a source (vs. finding a source). */
    op.kind = src_is_var ? EcsRuleAnd : EcsRuleWith;
    op.field_index = flecs_ito(int8_t, term->field_index);
    op.term_index = flecs_ito(int8_t, term - rule->filter.terms);

    /* If rule is transitive, use Trav(ersal) instruction */
    if (term->flags & EcsTermTransitive) {
        ecs_assert(ecs_term_id_is_set(&term->second), ECS_INTERNAL_ERROR, NULL);
        op.kind = EcsRuleTrav;
    } else {
        if (term->flags & (EcsTermMatchAny|EcsTermMatchAnySrc)) {
            op.kind = EcsRuleAndAny;
        }
    }

    /* If term has fixed id, insert simpler instruction that skips dealing with
     * wildcard terms and variables */
    if (flecs_rule_term_fixed_id(&rule->filter, term)) {
        if (op.kind == EcsRuleAnd) {
            op.kind = EcsRuleAndId;
        }
    }

    /* Save write state at start of term so we can use it to reliably track
     * variables got written by this term. */
    ecs_write_flags_t cond_write_state = ctx->cond_written;
    ecs_write_flags_t write_state = ctx->written;

    /* Resolve component inheritance if necessary */
    ecs_var_id_t first_var = EcsVarNone, second_var = EcsVarNone, src_var = EcsVarNone;

    /* Resolve variables and entities for operation arguments */
    flecs_rule_compile_term_id(world, rule, &op, &term->first, 
        &op.first, EcsRuleFirst, EcsVarEntity, ctx);
    flecs_rule_compile_term_id(world, rule, &op, &term->second, 
        &op.second, EcsRuleSecond, EcsVarEntity, ctx);

    if (first_is_var) first_var = op.first.var;
    if (second_is_var) second_var = op.second.var;

    /* Insert each instructions for table -> entity variable if needed */
    bool first_written = flecs_rule_compile_ensure_vars(
        rule, &op, &op.first, EcsRuleFirst, ctx, cond_write);
    bool second_written = flecs_rule_compile_ensure_vars(
        rule, &op, &op.second, EcsRuleSecond, ctx, cond_write);

    /* Do src last, in case it uses the same variable as first/second */
    flecs_rule_compile_term_id(world, rule, &op, &term->src, 
        &op.src, EcsRuleSrc, EcsVarAny, ctx);
    if (src_is_var) src_var = op.src.var;
    bool src_written = flecs_rule_is_written(src_var, ctx->written);

    /* Cache the current value of op.first. This value may get overwritten with
     * a variable when term has component inheritance, but Not operations may 
     * need the original value to initialize the result id with. */
    ecs_rule_ref_t prev_first = op.first;
    ecs_flags8_t prev_op_flags = op.flags;

    /* If the query starts with a Not or Optional term, insert an operation that
     * matches all entities. */
    if (first_term && src_is_var && !src_written) {
        bool pred_match = builtin_pred && term->first.id == EcsPredMatch;
        if (term->oper == EcsNot || term->oper == EcsOptional || pred_match) {
            ecs_rule_op_t match_any = {0};
            match_any.kind = EcsAnd;
            match_any.flags = EcsRuleIsSelf | (EcsRuleIsEntity << EcsRuleFirst);
            match_any.flags |= (EcsRuleIsVar << EcsRuleSrc);
            match_any.src = op.src;
            match_any.field_index = -1;
            if (!pred_match) {
                match_any.first.entity = EcsAny;
            } else {
                /* If matching by name, instead of finding all tables, just find
                 * the ones with a name. */
                match_any.first.entity = ecs_id(EcsIdentifier);
                match_any.second.entity = EcsName;
                match_any.flags |= (EcsRuleIsEntity << EcsRuleSecond);
            }
            match_any.written = (1ull << src_var);
            flecs_rule_op_insert(&match_any, ctx);
            flecs_rule_write_ctx(op.src.var, ctx, false);

            /* Update write administration */
            src_written = true;
        }
    }

    /* A bit of special logic for OR expressions and equality predicates. If the
     * left-hand of an equality operator is a table, and there are multiple
     * operators in an Or expression, the Or chain should match all entities in
     * the table that match the right hand sides of the operator expressions. 
     * For this to work, the src variable needs to be resolved as entity, as an
     * Or chain would otherwise only yield the first match from a table. */
    if (src_is_var && src_written && builtin_pred && term->oper == EcsOr) {
        /* Or terms are required to have the same source, so we don't have to
         * worry about the last term in the chain. */
        if (rule->vars[src_var].kind == EcsVarTable) {
            flecs_rule_compile_term_id(world, rule, &op, &term->src, 
                    &op.src, EcsRuleSrc, EcsVarEntity, ctx);
            src_var = op.src.var;
        }
    }

    flecs_rule_compile_ensure_vars(rule, &op, &op.src, EcsRuleSrc, ctx, cond_write);

    /* If source is Any (_) and first and/or second are unconstrained, insert an
     * ids instruction instead of an And */
    if (term->flags & EcsTermMatchAnySrc) {
        /* Use up-to-date written values after potentially inserting each */
        if (!first_written || !second_written) {
            if (!first_written) {
                /* If first is unknown, traverse left: <- (*, t) */
                op.kind = EcsRuleIdsLeft;
            } else {
                /* If second is wildcard, traverse right: (r, *) -> */
                op.kind = EcsRuleIdsRight;
            }
            op.src.entity = 0;
            op.flags &= (ecs_flags8_t)~(EcsRuleIsVar << EcsRuleSrc); /* ids has no src */
            op.flags &= (ecs_flags8_t)~(EcsRuleIsEntity << EcsRuleSrc);
        }
    }

    /* If this is a transitive term and both the target and source are unknown,
     * find the targets for the relationship first. This clusters together 
     * tables for the same target, which allows for more efficient usage of the
     * traversal caches. */
    if (term->flags & EcsTermTransitive && src_is_var && second_is_var) {
        if (!src_written && !second_written) {
            flecs_rule_insert_unconstrained_transitive(
                rule, &op, ctx, cond_write);
        }
    }

    /* If term has component inheritance enabled, insert instruction to walk
     * down the relationship tree of the id. */
    if (term->flags & EcsTermIdInherited) {
        if (is_not) {
            /* Ensure that term only matches if none of the inherited ids match
             * with the source. */
            flecs_rule_begin_none(ctx);
        }
        flecs_rule_insert_inheritance(rule, term, &op, ctx, cond_write);
    }

    /* If previous term was ScopeOpen with a Not operator, insert operation to
     * ensure that none of the results inside the scope should match. */
    if (!first_term && term[-1].first.id == EcsScopeOpen) {
        if (term[-1].oper == EcsNot) {
            flecs_rule_begin_none(ctx);
            flecs_rule_begin_not(ctx);
        }
        flecs_rule_compile_push(ctx);
    }

    /* Handle Not, Optional, Or operators */
    if (is_not) {
        flecs_rule_begin_not(ctx);
    } else if (term->oper == EcsOptional) {
        flecs_rule_begin_option(ctx);
    } else if (term->oper == EcsOr) {
        if (first_term || term[-1].oper != EcsOr) {
            if (!src_written) {
                flecs_rule_begin_union(ctx);
            }
        }
    }

    /* Check if this term has variables that have been conditionally written,
     * like variables written by an optional term. */
    if (ctx->cond_written) {
        flecs_rule_begin_cond_eval(&op, ctx, cond_write_state);
    }

    op.match_flags = term->flags;

    if (first_is_var) {
        op.first.var = first_var;
        op.flags &= (ecs_flags8_t)~(EcsRuleIsEntity << EcsRuleFirst);
        op.flags |= (EcsRuleIsVar << EcsRuleFirst);
    }

    if (term->src.flags & EcsSelf) {
        op.flags |= EcsRuleIsSelf;
    }

    if (builtin_pred) {
        if (flecs_rule_compile_builtin_pred(term, &op, write_state)) {
            return -1;
        }
    }

    flecs_rule_op_insert(&op, ctx);

    /* Handle self-references between src and first/second variables */
    if (src_is_var) {
        if (first_is_var) {
            flecs_rule_insert_contains(rule, src_var, first_var, ctx);
        }
        if (second_is_var && first_var != second_var) {
            flecs_rule_insert_contains(rule, src_var, second_var, ctx);
        }
    }

    /* Handle self references between first and second variables */
    if (first_is_var && !first_written && (first_var == second_var)) {
        flecs_rule_insert_pair_eq(term->field_index, ctx);
    }

    /* Handle closing of conditional evaluation */
    if (ctx->cond_written && (first_is_var || second_is_var || src_is_var)) {
        flecs_rule_end_cond_eval(&op, ctx);
    }

    /* Handle closing of Not, Optional and Or operators */
    if (is_not) {
        /* Restore original first id in case it got replaced with a variable */
        op.first = prev_first;
        op.flags = prev_op_flags;
        flecs_rule_end_not(&op, ctx, true);
    } else if (term->oper == EcsOptional) {
        flecs_rule_end_option(&op, ctx);
    } else if (term->oper == EcsOr) {
        if (ctx->cur->lbl_union != -1) {
            flecs_rule_next_or(ctx);
        } else {
            if (first_term || term[-1].oper != EcsOr) {
                if (ctx->cur->lbl_union == -1) {
                    flecs_rule_begin_or(ctx);
                }
            } else if (term->oper == EcsOr) {
                flecs_rule_next_or(ctx);
            }
        }
    } else if (term->oper == EcsAnd) {
        if (!first_term && term[-1].oper == EcsOr) {
            if (ctx->cur->lbl_union != -1) {
                flecs_rule_end_union(ctx);
            } else {
                flecs_rule_end_or(ctx);
            }
        }
    }

    return 0;
}

int flecs_rule_compile(
    ecs_world_t *world,
    ecs_stage_t *stage,
    ecs_rule_t *rule)
{
    ecs_filter_t *filter = &rule->filter;
    ecs_term_t *terms = filter->terms;
    ecs_rule_compile_ctx_t ctx = {0};
    ecs_vec_reset_t(NULL, &stage->operations, ecs_rule_op_t);
    ctx.ops = &stage->operations;
    ctx.cur = ctx.ctrlflow;
    ctx.cur->lbl_union = -1;
    ctx.cur->lbl_prev = -1;
    ctx.cur->lbl_not = -1;
    ctx.cur->lbl_none = -1;
    ecs_vec_clear(ctx.ops);

    /* Find all variables defined in query */
    flecs_rule_discover_vars(stage, rule);

    /* If rule contains fixed source terms, insert operation to set sources */
    int32_t i, count = filter->term_count;
    for (i = 0; i < count; i ++) {
        ecs_term_t *term = &terms[i];
        if (term->src.flags & EcsIsEntity) {
            ecs_rule_op_t set_fixed = {0};
            set_fixed.kind = EcsRuleSetFixed;
            flecs_rule_op_insert(&set_fixed, &ctx);
            break;
        }
    }

    /* If the rule contains terms with fixed ids (no wildcards, variables), 
     * insert instruction that initializes ecs_iter_t::ids. This allows for the
     * insertion of simpler instructions later on. */
    for (i = 0; i < count; i ++) {
        ecs_term_t *term = &terms[i];
        if (flecs_rule_term_fixed_id(filter, term)) {
            ecs_rule_op_t set_ids = {0};
            set_ids.kind = EcsRuleSetIds;
            flecs_rule_op_insert(&set_ids, &ctx);
            break;
        }
    }

    /* Compile query terms to instructions */
    for (i = 0; i < count; i ++) {
        ecs_term_t *term = &terms[i];
        if (flecs_rule_compile_term(world, rule, term, &ctx)) {
            return -1;
        }
    }

    /* If This variable has been written as entity, insert an operation to 
     * assign it to it.entities for consistency. */
    ecs_var_id_t this_id = flecs_rule_find_var_id(rule, "This", EcsVarEntity);
    if (this_id != EcsVarNone && (ctx.written & (1ull << this_id))) {
        ecs_rule_op_t set_this = {0};
        set_this.kind = EcsRuleSetThis;
        set_this.flags |= (EcsRuleIsVar << EcsRuleFirst);
        set_this.first.var = this_id;
        flecs_rule_op_insert(&set_this, &ctx);
    }

    /* Make sure non-This variables are written as entities */
    if (rule->vars) {
        for (i = 0; i < rule->var_count; i ++) {
            ecs_rule_var_t *var = &rule->vars[i];
            if (var->id && var->kind == EcsVarTable && var->name) {
                ecs_var_id_t var_id = flecs_rule_find_var_id(rule, var->name,
                    EcsVarEntity);
                if (!flecs_rule_is_written(var_id, ctx.written)) {
                    /* Skip anonymous variables */
                    if (!flecs_rule_var_is_anonymous(rule, var_id)) {
                        flecs_rule_insert_each(var->id, var_id, &ctx, false);
                    }
                }
            }
        }
    }

    /* If rule contains non-This variables as term source, build lookup array */
    if (rule->src_vars) {
        ecs_assert(rule->vars != NULL, ECS_INTERNAL_ERROR, NULL);
        bool only_anonymous = true;

        for (i = 0; i < filter->field_count; i ++) {
            ecs_var_id_t var_id = rule->src_vars[i];
            if (!var_id) {
                continue;
            }

            if (!flecs_rule_var_is_anonymous(rule, var_id)) {
                only_anonymous = false;
                break;
            } else {
                /* Don't fetch component data for anonymous variables. Because
                 * not all metadata (such as it.sources) is initialized for
                 * anonymous variables, and because they may only be available
                 * as table variables (each is not guaranteed to be inserted for
                 * anonymous variables) the iterator may not have sufficient
                 * information to resolve component data. */
                for (int32_t t = 0; t < filter->term_count; t ++) {
                    ecs_term_t *term = &filter->terms[t];
                    if (term->field_index == i) {
                        term->inout = EcsInOutNone;
                    }
                }
            }
        }

        /* Don't insert setvar instruction if all vars are anonymous */
        if (!only_anonymous) {
            ecs_rule_op_t set_vars = {0};
            set_vars.kind = EcsRuleSetVars;
            flecs_rule_op_insert(&set_vars, &ctx);
        }

        for (i = 0; i < filter->field_count; i ++) {
            ecs_var_id_t var_id = rule->src_vars[i];
            if (!var_id) {
                continue;
            }

            if (rule->vars[var_id].kind == EcsVarTable) {
                var_id = flecs_rule_find_var_id(rule, rule->vars[var_id].name,
                    EcsVarEntity);

                /* Variables used as source that aren't This must be entities */
                ecs_assert(var_id != EcsVarNone, ECS_INTERNAL_ERROR, NULL);
            }

            rule->src_vars[i] = var_id;
        }
    }

    /* If filter is empty, insert Nothing instruction */
    if (!rule->filter.term_count) {
        ecs_rule_op_t nothing = {0};
        nothing.kind = EcsRuleNothing;
        flecs_rule_op_insert(&nothing, &ctx);
    } else {
        /* Insert yield. If program reaches this operation, a result was found */
        ecs_rule_op_t yield = {0};
        yield.kind = EcsRuleYield;
        flecs_rule_op_insert(&yield, &ctx);
    }

    int32_t op_count = ecs_vec_count(ctx.ops);
    if (op_count) {
        rule->op_count = op_count;
        rule->ops = ecs_os_malloc_n(ecs_rule_op_t, op_count);
        ecs_rule_op_t *rule_ops = ecs_vec_first_t(ctx.ops, ecs_rule_op_t);
        ecs_os_memcpy_n(rule->ops, rule_ops, ecs_rule_op_t, op_count);
    }

    return 0;
}

#endif

 /**
 * @file addons/rules/api.c
 * @brief User facing API for rules.
 */

#include <ctype.h>

#ifdef FLECS_RULES

ecs_mixins_t ecs_rule_t_mixins = {
    .type_name = "ecs_rule_t",
    .elems = {
        [EcsMixinWorld] = offsetof(ecs_rule_t, filter.world),
        [EcsMixinEntity] = offsetof(ecs_rule_t, filter.entity),
        [EcsMixinIterable] = offsetof(ecs_rule_t, iterable),
        [EcsMixinDtor] = offsetof(ecs_rule_t, dtor)
    }
};

static
const char* flecs_rule_op_str(
    uint16_t kind)
{
    switch(kind) {
    case EcsRuleAnd:          return "and     ";
    case EcsRuleAndId:        return "and_id  ";
    case EcsRuleAndAny:       return "andany  ";
    case EcsRuleWith:         return "with    ";
    case EcsRuleTrav:         return "trav    ";
    case EcsRuleIdsRight:     return "idsr    ";
    case EcsRuleIdsLeft:      return "idsl    ";
    case EcsRuleEach:         return "each    ";
    case EcsRuleStore:        return "store   ";
    case EcsRuleUnion:        return "union   ";
    case EcsRuleEnd:          return "end     ";
    case EcsRuleNot:          return "not     ";
    case EcsRulePredEq:       return "eq      ";
    case EcsRulePredNeq:      return "neq     ";
    case EcsRulePredEqName:   return "eq_nm   ";
    case EcsRulePredNeqName:  return "neq_nm  ";
    case EcsRulePredEqMatch:  return "eq_m    ";
    case EcsRulePredNeqMatch: return "neq_m   ";
    case EcsRuleSetVars:      return "setvars ";
    case EcsRuleSetThis:      return "setthis ";
    case EcsRuleSetFixed:     return "setfix  ";
    case EcsRuleSetIds:       return "setids  ";
    case EcsRuleContain:      return "contain ";
    case EcsRulePairEq:       return "pair_eq ";
    case EcsRuleSetCond:      return "setcond ";
    case EcsRuleJmpCondFalse: return "jfalse  ";
    case EcsRuleJmpNotSet:    return "jnotset ";
    case EcsRuleYield:        return "yield   ";
    case EcsRuleNothing:      return "nothing ";
    default: return "!invalid";
    }
}

/* Implementation for iterable mixin */
static
void flecs_rule_iter_mixin_init(
    const ecs_world_t *world,
    const ecs_poly_t *poly,
    ecs_iter_t *iter,
    ecs_term_t *filter)
{
    ecs_poly_assert(poly, ecs_rule_t);

    if (filter) {
        iter[1] = ecs_rule_iter(world, (ecs_rule_t*)poly);
        iter[0] = ecs_term_chain_iter(&iter[1], filter);
    } else {
        iter[0] = ecs_rule_iter(world, (ecs_rule_t*)poly);
    }
}

static
void flecs_rule_fini(
    ecs_rule_t *rule)
{
    if (rule->vars != &rule->vars_cache.var) {
        ecs_os_free(rule->vars);
    }

    ecs_os_free(rule->ops);
    ecs_os_free(rule->src_vars);
    flecs_name_index_fini(&rule->tvar_index);
    flecs_name_index_fini(&rule->evar_index);
    ecs_filter_fini(&rule->filter);

    ecs_poly_free(rule, ecs_rule_t);
}

void ecs_rule_fini(
    ecs_rule_t *rule)
{
    if (rule->filter.entity) {
        /* If filter is associated with entity, use poly dtor path */
        ecs_delete(rule->filter.world, rule->filter.entity);
    } else {
        flecs_rule_fini(rule);
    }
}

ecs_rule_t* ecs_rule_init(
    ecs_world_t *world, 
    const ecs_filter_desc_t *const_desc)
{
    ecs_rule_t *result = ecs_poly_new(ecs_rule_t);
    ecs_stage_t *stage = flecs_stage_from_world(&world);

    /* Initialize the query */
    ecs_filter_desc_t desc = *const_desc;
    desc.storage = &result->filter; /* Use storage of rule */
    result->filter = ECS_FILTER_INIT;
    if (ecs_filter_init(world, &desc) == NULL) {
        goto error;
    }

    result->iterable.init = flecs_rule_iter_mixin_init;

    /* Compile filter to operations */
    if (flecs_rule_compile(world, stage, result)) {
        goto error;
    }

    ecs_entity_t entity = const_desc->entity;
    result->dtor = (ecs_poly_dtor_t)flecs_rule_fini;

    if (entity) {
        EcsPoly *poly = ecs_poly_bind(world, entity, ecs_rule_t);
        poly->poly = result;
        ecs_poly_modified(world, entity, ecs_rule_t);
    }

    return result;
error:
    ecs_rule_fini(result);
    return NULL;
}

static
int32_t flecs_rule_op_ref_str(
    const ecs_rule_t *rule,
    ecs_rule_ref_t *ref,
    ecs_flags16_t flags,
    ecs_strbuf_t *buf)
{
    int32_t color_chars = 0;
    if (flags & EcsRuleIsVar) {
        ecs_assert(ref->var < rule->var_count, ECS_INTERNAL_ERROR, NULL);
        ecs_rule_var_t *var = &rule->vars[ref->var];
        ecs_strbuf_appendlit(buf, "#[green]$#[reset]");
        if (var->kind == EcsVarTable) {
            ecs_strbuf_appendch(buf, '[');
        }
        ecs_strbuf_appendlit(buf, "#[green]");
        if (var->name) {
            ecs_strbuf_appendstr(buf, var->name);
        } else {
            if (var->id) {
#ifdef FLECS_DEBUG
                if (var->label) {
                    ecs_strbuf_appendstr(buf, var->label);
                    ecs_strbuf_appendch(buf, '\'');
                }
#endif
                ecs_strbuf_append(buf, "%d", var->id);
            } else {
                ecs_strbuf_appendlit(buf, "this");
            }
        }
        ecs_strbuf_appendlit(buf, "#[reset]");
        if (var->kind == EcsVarTable) {
            ecs_strbuf_appendch(buf, ']');
        }
        color_chars = ecs_os_strlen("#[green]#[reset]#[green]#[reset]");
    } else if (flags & EcsRuleIsEntity) {
        char *path = ecs_get_fullpath(rule->filter.world, ref->entity);
        ecs_strbuf_appendlit(buf, "#[blue]");
        ecs_strbuf_appendstr(buf, path);
        ecs_strbuf_appendlit(buf, "#[reset]");
        ecs_os_free(path);
        color_chars = ecs_os_strlen("#[blue]#[reset]");
    }
    return color_chars;
}

char* ecs_rule_str_w_profile(
    const ecs_rule_t *rule,
    const ecs_iter_t *it)
{
    ecs_poly_assert(rule, ecs_rule_t);

    ecs_strbuf_t buf = ECS_STRBUF_INIT;
    ecs_rule_op_t *ops = rule->ops;
    int32_t i, count = rule->op_count, indent = 0;
    for (i = 0; i < count; i ++) {
        ecs_rule_op_t *op = &ops[i];
        ecs_flags16_t flags = op->flags;
        ecs_flags16_t src_flags = flecs_rule_ref_flags(flags, EcsRuleSrc);
        ecs_flags16_t first_flags = flecs_rule_ref_flags(flags, EcsRuleFirst);
        ecs_flags16_t second_flags = flecs_rule_ref_flags(flags, EcsRuleSecond);

        if (it) {
#ifdef FLECS_DEBUG
            const ecs_rule_iter_t *rit = &it->priv.iter.rule;
            ecs_strbuf_append(&buf, 
                "#[green]%4d -> #[red]%4d <- #[grey]  |   ",
                rit->profile[i].count[0],
                rit->profile[i].count[1]);
#endif
        }

        ecs_strbuf_append(&buf, 
            "#[normal]%2d. [#[grey]%2d#[reset], #[green]%2d#[reset]]  ", 
                i, op->prev, op->next);
        int32_t hidden_chars, start = ecs_strbuf_written(&buf);
        if (op->kind == EcsRuleEnd) {
            indent --;
        }

        ecs_strbuf_append(&buf, "%*s", indent, "");
        ecs_strbuf_appendstr(&buf, flecs_rule_op_str(op->kind));
        ecs_strbuf_appendstr(&buf, " ");

        int32_t written = ecs_strbuf_written(&buf);
        for (int32_t j = 0; j < (10 - (written - start)); j ++) {
            ecs_strbuf_appendch(&buf, ' ');
        }

        if (op->kind == EcsRuleJmpCondFalse || op->kind == EcsRuleSetCond ||
            op->kind == EcsRuleJmpNotSet) 
        {
            ecs_strbuf_appendint(&buf, op->other);
            ecs_strbuf_appendch(&buf, ' ');
        }
    
        hidden_chars = flecs_rule_op_ref_str(rule, &op->src, src_flags, &buf);

        if (op->kind == EcsRuleUnion) {
            indent ++;
        }

        if (!first_flags && !second_flags) {
            ecs_strbuf_appendstr(&buf, "\n");
            continue;
        }

        written = ecs_strbuf_written(&buf) - hidden_chars;
        for (int32_t j = 0; j < (30 - (written - start)); j ++) {
            ecs_strbuf_appendch(&buf, ' ');
        }

        ecs_strbuf_appendstr(&buf, "(");
        flecs_rule_op_ref_str(rule, &op->first, first_flags, &buf);

        if (second_flags) {
            ecs_strbuf_appendstr(&buf, ", ");
            flecs_rule_op_ref_str(rule, &op->second, second_flags, &buf);
        } else {
            switch (op->kind) {
            case EcsRulePredEqName:
            case EcsRulePredNeqName:
            case EcsRulePredEqMatch:
            case EcsRulePredNeqMatch: {
                int8_t term_index = op->term_index;
                ecs_strbuf_appendstr(&buf, ", #[yellow]\"");
                ecs_strbuf_appendstr(&buf, rule->filter.terms[term_index].second.name);
                ecs_strbuf_appendstr(&buf, "\"#[reset]");
            }
            default:
                break;
            }
        }

        ecs_strbuf_appendch(&buf, ')');

        ecs_strbuf_appendch(&buf, '\n');
    }

#ifdef FLECS_LOG    
    char *str = ecs_strbuf_get(&buf);
    flecs_colorize_buf(str, true, &buf);
    ecs_os_free(str);
#endif
    return ecs_strbuf_get(&buf);
}

char* ecs_rule_str(
    const ecs_rule_t *rule)
{
    return ecs_rule_str_w_profile(rule, NULL);
}

const ecs_filter_t* ecs_rule_get_filter(
    const ecs_rule_t *rule)
{
    return &rule->filter;
}

const char* ecs_rule_parse_vars(
    ecs_rule_t *rule,
    ecs_iter_t *it,
    const char *expr)
{
    ecs_poly_assert(rule, ecs_rule_t);
    ecs_check(it != NULL, ECS_INVALID_PARAMETER, NULL);
    ecs_check(expr != NULL, ECS_INVALID_PARAMETER, NULL)
    char token[ECS_MAX_TOKEN_SIZE];
    const char *ptr = expr;
    bool paren = false;

    const char *name = NULL;
    if (rule->filter.entity) {
        name = ecs_get_name(rule->filter.world, rule->filter.entity);
    }

    ptr = ecs_parse_ws_eol(ptr);
    if (!ptr[0]) {
        return ptr;
    }

    if (ptr[0] == '(') {
        paren = true;
        ptr = ecs_parse_ws_eol(ptr + 1);
        if (ptr[0] == ')') {
            return ptr + 1;
        }
    }

    do {
        ptr = ecs_parse_ws_eol(ptr);
        ptr = ecs_parse_identifier(name, expr, ptr, token);
        if (!ptr) {
            return NULL;
        }

        int var = ecs_rule_find_var(rule, token);
        if (var == -1) {
            ecs_parser_error(name, expr, (ptr - expr), 
                "unknown variable '%s'", token);
            return NULL;
        }

        ptr = ecs_parse_ws_eol(ptr);
        if (ptr[0] != ':') {
            ecs_parser_error(name, expr, (ptr - expr), 
                "missing ':'");
            return NULL;
        }

        ptr = ecs_parse_ws_eol(ptr + 1);
        ptr = ecs_parse_identifier(name, expr, ptr, token);
        if (!ptr) {
            return NULL;
        }

        ecs_entity_t val = ecs_lookup_fullpath(rule->filter.world, token);
        if (!val) {
            ecs_parser_error(name, expr, (ptr - expr), 
                "unresolved entity '%s'", token);
            return NULL;
        }

        ecs_iter_set_var(it, var, val);

        ptr = ecs_parse_ws_eol(ptr);
        if (ptr[0] == ')') {
            if (!paren) {
                ecs_parser_error(name, expr, (ptr - expr), 
                    "unexpected closing parenthesis");
                return NULL;
            }

            ptr ++;
            break;
        } else if (ptr[0] == ',') {
            ptr ++;
        } else if (!ptr[0]) {
            if (paren) {
                ecs_parser_error(name, expr, (ptr - expr), 
                    "missing closing parenthesis");
                return NULL;
            }
            break;
        } else {
            ecs_parser_error(name, expr, (ptr - expr), 
                "expected , or end of string");
            return NULL;
        }
    } while (true);

    return ptr;
error:
    return NULL;
}

#endif

/**
 * @file addons/rules/trav_cache.c
 * @brief Cache that stores the result of graph traversal.
 */


#ifdef FLECS_RULES

static
void flecs_rule_build_down_cache(
    ecs_world_t *world,
    ecs_allocator_t *a,
    const ecs_rule_run_ctx_t *ctx,
    ecs_trav_cache_t *cache,
    ecs_entity_t trav,
    ecs_entity_t entity)
{
    ecs_id_record_t *idr = flecs_id_record_get(world, ecs_pair(trav, entity));
    if (!idr) {
        return;
    }

    ecs_trav_elem_t *elem = ecs_vec_append_t(a, &cache->entities, 
        ecs_trav_elem_t);
    elem->entity = entity;
    elem->idr = idr;

    ecs_table_cache_iter_t it;
    if (flecs_table_cache_iter(&idr->cache, &it)) {
        ecs_table_record_t *tr; 
        while ((tr = flecs_table_cache_next(&it, ecs_table_record_t))) {
            ecs_assert(tr->count == 1, ECS_INTERNAL_ERROR, NULL);
            ecs_table_t *table = tr->hdr.table;
            if (!table->_->traversable_count) {
                continue;
            }

            int32_t i, count = ecs_table_count(table);
            ecs_record_t **records = table->data.records.array;
            ecs_entity_t *entities = table->data.entities.array;
            for (i = 0; i < count; i ++) {
                ecs_record_t *r = records[i];
                if (r->row & EcsEntityIsTraversable) {
                    flecs_rule_build_down_cache(
                        world, a, ctx, cache, trav, entities[i]);
                }
            }
        }
    }
}

static
void flecs_rule_build_up_cache(
    ecs_world_t *world,
    ecs_allocator_t *a,
    const ecs_rule_run_ctx_t *ctx,
    ecs_trav_cache_t *cache,
    ecs_entity_t trav,
    ecs_table_t *table,
    const ecs_table_record_t *tr,
    int32_t root_column)
{
    ecs_id_t *ids = table->type.array;
    int32_t i = tr->column, end = i + tr->count;
    bool is_root = root_column == -1;

    for (; i < end; i ++) {
        ecs_entity_t second = ecs_pair_second(world, ids[i]);
        if (is_root) {
            root_column = i;
        }

        ecs_trav_elem_t *el = ecs_vec_append_t(a, &cache->entities, 
            ecs_trav_elem_t);
        el->entity = second;
        el->column = root_column;
        el->idr = NULL;

        ecs_record_t *r = flecs_entities_get_any(world, second);
        if (r->table) {
            const ecs_table_record_t *r_tr = flecs_id_record_get_table(
                cache->idr, r->table);
            if (!r_tr) {
                return;
            }
            flecs_rule_build_up_cache(world, a, ctx, cache, trav, r->table, 
                r_tr, root_column);
        }
    }
}

void flecs_rule_trav_cache_fini(
    ecs_allocator_t *a,
    ecs_trav_cache_t *cache)
{
    ecs_vec_fini_t(a, &cache->entities, ecs_trav_elem_t);
}

void flecs_rule_get_down_cache(
    const ecs_rule_run_ctx_t *ctx,
    ecs_trav_cache_t *cache,
    ecs_entity_t trav,
    ecs_entity_t entity)
{
    if (cache->id != ecs_pair(trav, entity) || cache->up) {
        ecs_world_t *world = ctx->it->real_world;
        ecs_allocator_t *a = flecs_rule_get_allocator(ctx->it);
        ecs_vec_reset_t(a, &cache->entities, ecs_trav_elem_t);
        flecs_rule_build_down_cache(world, a, ctx, cache, trav, entity);
        cache->id = ecs_pair(trav, entity);
        cache->up = false;
    }
}

void flecs_rule_get_up_cache(
    const ecs_rule_run_ctx_t *ctx,
    ecs_trav_cache_t *cache,
    ecs_entity_t trav,
    ecs_table_t *table)
{
    ecs_assert(table != NULL, ECS_INTERNAL_ERROR, NULL);
    ecs_world_t *world = ctx->it->real_world;
    ecs_allocator_t *a = flecs_rule_get_allocator(ctx->it);

    ecs_id_record_t *idr = cache->idr;
    if (!idr || idr->id != ecs_pair(trav, EcsWildcard)) {
        idr = cache->idr = flecs_id_record_get(world, 
            ecs_pair(trav, EcsWildcard));
        if (!idr) {
            ecs_vec_reset_t(a, &cache->entities, ecs_trav_elem_t);
            return;
        }
    }

    const ecs_table_record_t *tr = flecs_id_record_get_table(idr, table);
    if (!tr) {
        ecs_vec_reset_t(a, &cache->entities, ecs_trav_elem_t);
        return;
    }

    ecs_id_t id = table->type.array[tr->column];

    if (cache->id != id || !cache->up) {
        ecs_vec_reset_t(a, &cache->entities, ecs_trav_elem_t);
        flecs_rule_build_up_cache(world, a, ctx, cache, trav, table, tr, -1);
        cache->id = id;
        cache->up = true;
    }
}

#endif

/**
 * @file addons/rules/engine.c
 * @brief Rules engine implementation.
 */


#ifdef FLECS_RULES

ecs_allocator_t* flecs_rule_get_allocator(
    const ecs_iter_t *it)
{
    ecs_world_t *world = it->world;
    if (ecs_poly_is(world, ecs_world_t)) {
        return &world->allocator;
    } else {
        ecs_assert(ecs_poly_is(world, ecs_stage_t), ECS_INTERNAL_ERROR, NULL);
        return &((ecs_stage_t*)world)->allocator;
    }
}

static
ecs_rule_op_ctx_t* _flecs_op_ctx(
    const ecs_rule_run_ctx_t *ctx)
{
    return &ctx->op_ctx[ctx->op_index];
}

#define flecs_op_ctx(ctx, op_kind) (&_flecs_op_ctx(ctx)->is.op_kind)

static
ecs_table_range_t flecs_range_from_entity(
    ecs_entity_t e,
    const ecs_rule_run_ctx_t *ctx)
{
    ecs_record_t *r = flecs_entities_get(ctx->world, e);
    if (!r) {
        return (ecs_table_range_t){ 0 };
    }
    return (ecs_table_range_t){
        .table = r->table,
        .offset = ECS_RECORD_TO_ROW(r->row),
        .count = 1
    };
}

static
ecs_table_range_t flecs_rule_var_get_range(
    int32_t var_id,
    const ecs_rule_run_ctx_t *ctx)
{
    ecs_assert(var_id < ctx->rule->var_count, ECS_INTERNAL_ERROR, NULL);
    ecs_var_t *var = &ctx->vars[var_id];
    ecs_table_t *table = var->range.table;
    if (table) {
        return var->range;
    }

    ecs_entity_t entity = var->entity;
    if (entity && entity != EcsWildcard) {
        var->range = flecs_range_from_entity(entity, ctx);
        return var->range;
    }

    return (ecs_table_range_t){ 0 };
}

static
ecs_table_t* flecs_rule_var_get_table(
    int32_t var_id,
    const ecs_rule_run_ctx_t *ctx)
{
    ecs_var_t *var = &ctx->vars[var_id];
    ecs_table_t *table = var->range.table;
    if (table) {
        return table;
    }

    ecs_entity_t entity = var->entity;
    if (entity && entity != EcsWildcard) {
        var->range = flecs_range_from_entity(entity, ctx);
        return var->range.table;
    }

    return NULL;
}

static
ecs_table_t* flecs_rule_get_table(
    const ecs_rule_op_t *op,
    const ecs_rule_ref_t *ref,
    ecs_flags16_t ref_kind,
    const ecs_rule_run_ctx_t *ctx)
{
    ecs_flags16_t flags = flecs_rule_ref_flags(op->flags, ref_kind);
    if (flags & EcsRuleIsEntity) {
        return ecs_get_table(ctx->world, ref->entity);
    } else {
        return flecs_rule_var_get_table(ref->var, ctx);
    }
}

static
ecs_table_range_t flecs_rule_get_range(
    const ecs_rule_op_t *op,
    const ecs_rule_ref_t *ref,
    ecs_flags16_t ref_kind,
    const ecs_rule_run_ctx_t *ctx)
{
    ecs_flags16_t flags = flecs_rule_ref_flags(op->flags, ref_kind);
    if (flags & EcsRuleIsEntity) {
        ecs_assert(!(flags & EcsRuleIsVar), ECS_INTERNAL_ERROR, NULL);
        return flecs_range_from_entity(ref->entity, ctx);
    } else {
        ecs_var_t *var = &ctx->vars[ref->var];
        if (var->range.table) {
            return ctx->vars[ref->var].range;
        } else if (var->entity) {
            return flecs_range_from_entity(var->entity, ctx);
        }
    }
    return (ecs_table_range_t){0};
}

static
ecs_entity_t flecs_rule_var_get_entity(
    ecs_var_id_t var_id,
    const ecs_rule_run_ctx_t *ctx)
{
    ecs_assert(var_id < (ecs_var_id_t)ctx->rule->var_count, 
        ECS_INTERNAL_ERROR, NULL);
    ecs_var_t *var = &ctx->vars[var_id];
    ecs_entity_t entity = var->entity;
    if (entity) {
        return entity;
    }

    ecs_assert(var->range.count == 1, ECS_INTERNAL_ERROR, NULL);
    ecs_table_t *table = var->range.table;
    ecs_entity_t *entities = table->data.entities.array;
    var->entity = entities[var->range.offset];
    return var->entity;
}

static
void flecs_rule_var_reset(
    ecs_var_id_t var_id,
    const ecs_rule_run_ctx_t *ctx)
{
    ctx->vars[var_id].entity = EcsWildcard;
    ctx->vars[var_id].range.table = NULL;
}

static
void flecs_rule_var_set_table(
    const ecs_rule_op_t *op,
    ecs_var_id_t var_id,
    ecs_table_t *table,
    int32_t offset,
    int32_t count,
    const ecs_rule_run_ctx_t *ctx)
{
    (void)op;
    ecs_assert(ctx->rule_vars[var_id].kind == EcsVarTable, 
        ECS_INTERNAL_ERROR, NULL);
    ecs_assert(flecs_rule_is_written(var_id, op->written), 
        ECS_INTERNAL_ERROR, NULL);
    ecs_var_t *var = &ctx->vars[var_id];
    var->entity = 0;
    var->range = (ecs_table_range_t){ 
        .table = table,
        .offset = offset,
        .count = count
    };
}

static
void flecs_rule_var_set_entity(
    const ecs_rule_op_t *op,
    ecs_var_id_t var_id,
    ecs_entity_t entity,
    const ecs_rule_run_ctx_t *ctx)
{
    (void)op;
    ecs_assert(var_id < (ecs_var_id_t)ctx->rule->var_count, 
        ECS_INTERNAL_ERROR, NULL);
    ecs_assert(flecs_rule_is_written(var_id, op->written), 
        ECS_INTERNAL_ERROR, NULL);
    ecs_var_t *var = &ctx->vars[var_id];
    var->range.table = NULL;
    var->entity = entity;
}

static
void flecs_rule_set_vars(
    const ecs_rule_op_t *op,
    ecs_id_t id,
    const ecs_rule_run_ctx_t *ctx)
{
    ecs_flags16_t flags_1st = flecs_rule_ref_flags(op->flags, EcsRuleFirst);
    ecs_flags16_t flags_2nd = flecs_rule_ref_flags(op->flags, EcsRuleSecond);

    if (flags_1st & EcsRuleIsVar) {
        ecs_var_id_t var = op->first.var;
        if (op->written & (1ull << var)) {
            if (ECS_IS_PAIR(id)) {
                flecs_rule_var_set_entity(
                    op, var, ecs_pair_first(ctx->world, id), ctx);
            } else {
                flecs_rule_var_set_entity(op, var, id, ctx);
            }
        }
    }
    if (flags_2nd & EcsRuleIsVar) {
        ecs_var_id_t var = op->second.var;
        if (op->written & (1ull << var)) {
            flecs_rule_var_set_entity(
                op, var, ecs_pair_second(ctx->world, id), ctx);
        }
    }
}

static
ecs_table_range_t flecs_get_ref_range(
    const ecs_rule_ref_t *ref,
    ecs_flags16_t flag,
    const ecs_rule_run_ctx_t *ctx)
{
    if (flag & EcsRuleIsEntity) {
        return flecs_range_from_entity(ref->entity, ctx);
    } else if (flag & EcsRuleIsVar) {
        return flecs_rule_var_get_range(ref->var, ctx);
    }
    return (ecs_table_range_t){0};
}

static
ecs_entity_t flecs_get_ref_entity(
    const ecs_rule_ref_t *ref,
    ecs_flags16_t flag,
    const ecs_rule_run_ctx_t *ctx)
{
    if (flag & EcsRuleIsEntity) {
        return ref->entity;
    } else if (flag & EcsRuleIsVar) {
        return flecs_rule_var_get_entity(ref->var, ctx);
    }
    return 0;
}

static
ecs_id_t flecs_rule_op_get_id_w_written(
    const ecs_rule_op_t *op,
    uint64_t written,
    const ecs_rule_run_ctx_t *ctx)
{
    ecs_flags16_t flags_1st = flecs_rule_ref_flags(op->flags, EcsRuleFirst);
    ecs_flags16_t flags_2nd = flecs_rule_ref_flags(op->flags, EcsRuleSecond);
    ecs_entity_t first = 0, second = 0;

    if (flags_1st) {
        if (flecs_ref_is_written(op, &op->first, EcsRuleFirst, written)) {
            first = flecs_get_ref_entity(&op->first, flags_1st, ctx);
        } else if (flags_1st & EcsRuleIsVar) {
            first = EcsWildcard;
        }
    }
    if (flags_2nd) {
        if (flecs_ref_is_written(op, &op->second, EcsRuleSecond, written)) {
            second = flecs_get_ref_entity(&op->second, flags_2nd, ctx);
        } else if (flags_2nd & EcsRuleIsVar) {
            second = EcsWildcard;
        }
    }

    if (flags_2nd & (EcsRuleIsVar | EcsRuleIsEntity)) {
        return ecs_pair(first, second);
    } else {
        return ecs_get_alive(ctx->world, first);
    }
}

static
ecs_id_t flecs_rule_op_get_id(
    const ecs_rule_op_t *op,
    const ecs_rule_run_ctx_t *ctx)
{
    uint64_t written = ctx->written[ctx->op_index];
    return flecs_rule_op_get_id_w_written(op, written, ctx);
}

static
int16_t flecs_rule_next_column(
    ecs_table_t *table,
    ecs_id_t id,
    int32_t column)
{
    if (!ECS_IS_PAIR(id) || (ECS_PAIR_FIRST(id) != EcsWildcard)) {
        column = column + 1;
    } else {
        ecs_assert(column >= 0, ECS_INTERNAL_ERROR, NULL);
        column = ecs_search_offset(NULL, table, column + 1, id, NULL);
        ecs_assert(column != -1, ECS_INTERNAL_ERROR, NULL);
    }
    return flecs_ito(int16_t, column);
}

static
void flecs_rule_it_set_column(
    ecs_iter_t *it,
    int32_t field_index,
    int32_t column)
{
    ecs_assert(column >= 0, ECS_INTERNAL_ERROR, NULL);
    ecs_assert(field_index >= 0, ECS_INTERNAL_ERROR, NULL);
    it->columns[field_index] = column + 1;
    if (it->sources[field_index] != 0) {
        it->columns[field_index] *= -1;
    }
}

static
ecs_id_t flecs_rule_it_set_id(
    ecs_iter_t *it,
    ecs_table_t *table,
    int32_t field_index,
    int32_t column)
{
    ecs_assert(column >= 0, ECS_INTERNAL_ERROR, NULL);
    ecs_assert(field_index >= 0, ECS_INTERNAL_ERROR, NULL);
    return it->ids[field_index] = table->type.array[column];
}

static
void flecs_rule_set_match(
    const ecs_rule_op_t *op,
    ecs_table_t *table,
    int32_t column,
    const ecs_rule_run_ctx_t *ctx)
{
    ecs_assert(column >= 0, ECS_INTERNAL_ERROR, NULL);
    int32_t field_index = op->field_index;
    if (field_index == -1) {
        return;
    }

    ecs_iter_t *it = ctx->it;
    flecs_rule_it_set_column(it, field_index, column);
    ecs_id_t matched = flecs_rule_it_set_id(it, table, field_index, column);
    flecs_rule_set_vars(op, matched, ctx);
}

static
void flecs_rule_set_trav_match(
    const ecs_rule_op_t *op,
    int32_t column,
    ecs_entity_t trav,
    ecs_entity_t second,
    const ecs_rule_run_ctx_t *ctx)
{
    int32_t field_index = op->field_index;
    if (field_index == -1) {
        return;
    }

    ecs_iter_t *it = ctx->it;
    ecs_id_t matched = ecs_pair(trav, second);
    it->ids[op->field_index] = matched;
    if (column != -1) {
        flecs_rule_it_set_column(it, op->field_index, column);
    }
    flecs_rule_set_vars(op, matched, ctx);
}

static
bool flecs_rule_select_w_id(
    const ecs_rule_op_t *op,
    bool redo,
    const ecs_rule_run_ctx_t *ctx,
    ecs_id_t id)
{
    ecs_rule_and_ctx_t *op_ctx = flecs_op_ctx(ctx, and);
    ecs_id_record_t *idr = op_ctx->idr;
    ecs_table_record_t *tr;
    ecs_table_t *table;

    if (!redo) {
        if (!idr || idr->id != id) {
            idr = op_ctx->idr = flecs_id_record_get(ctx->world, id);
            if (!idr) {
                return false;
            }
        }

        if (!flecs_table_cache_iter(&idr->cache, &op_ctx->it)) {
            return false;
        }
    }

    if (!redo || !op_ctx->remaining) {
        tr = flecs_table_cache_next(&op_ctx->it, ecs_table_record_t);
        if (!tr) {
            return false;
        }

        op_ctx->column = flecs_ito(int16_t, tr->column);
        op_ctx->remaining = flecs_ito(int16_t, tr->count - 1);
        table = tr->hdr.table;
        flecs_rule_var_set_table(op, op->src.var, table, 0, 0, ctx);
    } else {
        tr = (ecs_table_record_t*)op_ctx->it.cur;
        ecs_assert(tr != NULL, ECS_INTERNAL_ERROR, NULL);
        table = tr->hdr.table;
        op_ctx->column = flecs_rule_next_column(table, idr->id, op_ctx->column);
        op_ctx->remaining --;
    }

    flecs_rule_set_match(op, table, op_ctx->column, ctx);
    return true;
}

static
bool flecs_rule_select(
    const ecs_rule_op_t *op,
    bool redo,
    const ecs_rule_run_ctx_t *ctx)
{
    ecs_id_t id = 0;
    if (!redo) {
        id = flecs_rule_op_get_id(op, ctx);
    }
    return flecs_rule_select_w_id(op, redo, ctx, id);
}

static
bool flecs_rule_with(
    const ecs_rule_op_t *op,
    bool redo,
    const ecs_rule_run_ctx_t *ctx)
{
    ecs_rule_and_ctx_t *op_ctx = flecs_op_ctx(ctx, and);
    ecs_id_record_t *idr = op_ctx->idr;
    const ecs_table_record_t *tr;

    ecs_table_t *table = flecs_rule_get_table(op, &op->src, EcsRuleSrc, ctx);
    if (!table) {
        return false;
    }

    if (!redo) {
        ecs_id_t id = flecs_rule_op_get_id(op, ctx);
        if (!idr || idr->id != id) {
            idr = op_ctx->idr = flecs_id_record_get(ctx->world, id);
            if (!idr) {
                return false;
            }
        }

        tr = flecs_id_record_get_table(idr, table);
        if (!tr) {
            return false;
        }

        op_ctx->column = flecs_ito(int16_t, tr->column);
        op_ctx->remaining = flecs_ito(int16_t, tr->count);
    } else {
        if (--op_ctx->remaining <= 0) {
            return false;
        }

        op_ctx->column = flecs_rule_next_column(table, idr->id, op_ctx->column);
        ecs_assert(op_ctx->column != -1, ECS_INTERNAL_ERROR, NULL);
    }

    flecs_rule_set_match(op, table, op_ctx->column, ctx);
    return true;
}

static
bool flecs_rule_and(
    const ecs_rule_op_t *op,
    bool redo,
    const ecs_rule_run_ctx_t *ctx)
{
    uint64_t written = ctx->written[ctx->op_index];
    if (written & (1ull << op->src.var)) {
        return flecs_rule_with(op, redo, ctx);
    } else {
        return flecs_rule_select(op, redo, ctx);
    }
}

static
bool flecs_rule_select_id(
    const ecs_rule_op_t *op,
    bool redo,
    const ecs_rule_run_ctx_t *ctx)
{
    ecs_rule_and_ctx_t *op_ctx = flecs_op_ctx(ctx, and);
    ecs_iter_t *it = ctx->it;
    int8_t field = op->field_index;
    ecs_assert(field != -1, ECS_INTERNAL_ERROR, NULL);

    if (!redo) {
        ecs_id_t id = it->ids[field];
        ecs_id_record_t *idr = op_ctx->idr;
        if (!idr || idr->id != id) {
            idr = op_ctx->idr = flecs_id_record_get(ctx->world, id);
            if (!idr) {
                return false;
            }
        }

        if (!flecs_table_cache_iter(&idr->cache, &op_ctx->it)) {
            return false;
        }
    }

    const ecs_table_record_t *tr = flecs_table_cache_next(
        &op_ctx->it, ecs_table_record_t);
    if (!tr) {
        return false;
    }

    ecs_table_t *table = tr->hdr.table;
    flecs_rule_var_set_table(op, op->src.var, table, 0, 0, ctx);
    flecs_rule_it_set_column(it, field, tr->column);
    return true;
}

static
bool flecs_rule_with_id(
    const ecs_rule_op_t *op,
    bool redo,
    const ecs_rule_run_ctx_t *ctx)
{
    if (redo) {
        return false;
    }

    ecs_rule_and_ctx_t *op_ctx = flecs_op_ctx(ctx, and);
    ecs_iter_t *it = ctx->it;
    int8_t field = op->field_index;
    ecs_assert(field != -1, ECS_INTERNAL_ERROR, NULL);

    ecs_table_t *table = flecs_rule_get_table(op, &op->src, EcsRuleSrc, ctx);
    if (!table) {
        return false;
    }

    ecs_id_t id = it->ids[field];
    ecs_id_record_t *idr = op_ctx->idr;
    if (!idr || idr->id != id) {
        idr = op_ctx->idr = flecs_id_record_get(ctx->world, id);
        if (!idr) {
            return false;
        }
    }

    const ecs_table_record_t *tr = flecs_id_record_get_table(idr, table);
    if (!tr) {
        return false;
    }

    flecs_rule_it_set_column(it, field, tr->column);
    return true;
}

static
bool flecs_rule_and_id(
    const ecs_rule_op_t *op,
    bool redo,
    const ecs_rule_run_ctx_t *ctx)
{
    uint64_t written = ctx->written[ctx->op_index];
    if (written & (1ull << op->src.var)) {
        return flecs_rule_with_id(op, redo, ctx);
    } else {
        return flecs_rule_select_id(op, redo, ctx);
    }
}

static
bool flecs_rule_and_any(
    const ecs_rule_op_t *op,
    bool redo,
    const ecs_rule_run_ctx_t *ctx)
{
    ecs_flags16_t match_flags = op->match_flags;
    if (redo) {
        if (match_flags & EcsTermMatchAnySrc) {
            return false;
        }
    }

    uint64_t written = ctx->written[ctx->op_index];
    int32_t remaining = 1;
    bool result;
    if (flecs_ref_is_written(op, &op->src, EcsRuleSrc, written)) {
        result = flecs_rule_with(op, redo, ctx);
    } else {
        result = flecs_rule_select(op, redo, ctx);
        remaining = 0;
    }

    if (!redo) {
        ecs_rule_and_ctx_t *op_ctx = flecs_op_ctx(ctx, and);
        if (match_flags & EcsTermMatchAny && op_ctx->remaining) {
            op_ctx->remaining = flecs_ito(int16_t, remaining);
        }
    }

    int32_t field = op->field_index;
    if (field != -1) {
        ctx->it->ids[field] = flecs_rule_op_get_id(op, ctx);
    }

    return result;
}

static
bool flecs_rule_trav_fixed_src_reflexive(
    const ecs_rule_op_t *op,
    const ecs_rule_run_ctx_t *ctx,
    ecs_table_range_t *range,
    ecs_entity_t trav,
    ecs_entity_t second)
{
    ecs_table_t *table = range->table;
    ecs_entity_t *entities = table->data.entities.array;
    int32_t count = range->count;
    if (!count) {
        count = ecs_table_count(table);
    }

    int32_t i = range->offset, end = i + count;
    for (; i < end; i ++) {
        if (entities[i] == second) {
            /* Even though table doesn't have the specific relationship 
             * pair, the relationship is reflexive and the target entity
             * is stored in the table. */
            break;
        }
    }
    if (i == end) {
        /* Table didn't contain target entity */
        return false;
    }
    if (count > 1) {
        /* If the range contains more than one entity, set the range to
         * return only the entity matched by the reflexive property. */
        ecs_assert(flecs_rule_ref_flags(op->flags, EcsRuleSrc) & EcsRuleIsVar, 
            ECS_INTERNAL_ERROR, NULL);
        ecs_var_t *var = &ctx->vars[op->src.var];
        ecs_table_range_t *var_range = &var->range;
        var_range->offset = i;
        var_range->count = 1;
        var->entity = entities[i];
    }

    flecs_rule_set_trav_match(op, -1, trav, second, ctx);
    return true;
}

static
bool flecs_rule_trav_unknown_src_reflexive(
    const ecs_rule_op_t *op,
    const ecs_rule_run_ctx_t *ctx,
    ecs_entity_t trav,
    ecs_entity_t second)
{
    ecs_assert(flecs_rule_ref_flags(op->flags, EcsRuleSrc) & EcsRuleIsVar,
        ECS_INTERNAL_ERROR, NULL);
    ecs_var_id_t src_var = op->src.var;
    flecs_rule_var_set_entity(op, src_var, second, ctx);
    flecs_rule_var_get_table(src_var, ctx);
    flecs_rule_set_trav_match(op, -1, trav, second, ctx);
    return true;
}

static
bool flecs_rule_trav_fixed_src_up_fixed_second(
    const ecs_rule_op_t *op,
    bool redo,
    const ecs_rule_run_ctx_t *ctx)
{
    if (redo) {
        return false; /* If everything's fixed, can only have a single result */
    }

    ecs_flags16_t f_1st = flecs_rule_ref_flags(op->flags, EcsRuleFirst);
    ecs_flags16_t f_2nd = flecs_rule_ref_flags(op->flags, EcsRuleSecond);
    ecs_flags16_t f_src = flecs_rule_ref_flags(op->flags, EcsRuleSrc);
    ecs_entity_t trav = flecs_get_ref_entity(&op->first, f_1st, ctx);
    ecs_entity_t second = flecs_get_ref_entity(&op->second, f_2nd, ctx);
    ecs_table_range_t range = flecs_get_ref_range(&op->src, f_src, ctx);
    ecs_table_t *table = range.table;

    /* Check if table has transitive relationship by traversing upwards */
    int32_t column = ecs_search_relation(ctx->world, table, 0, 
        ecs_pair(trav, second), trav, EcsSelf|EcsUp, NULL, NULL, NULL);
    if (column == -1) {
        if (op->match_flags & EcsTermReflexive) {
            return flecs_rule_trav_fixed_src_reflexive(op, ctx,
                &range, trav, second);
        } else {
            return false;
        }
    }

    flecs_rule_set_trav_match(op, column, trav, second, ctx);
    return true;
}

static
bool flecs_rule_trav_unknown_src_up_fixed_second(
    const ecs_rule_op_t *op,
    bool redo,
    const ecs_rule_run_ctx_t *ctx)
{
    ecs_flags16_t f_1st = flecs_rule_ref_flags(op->flags, EcsRuleFirst);
    ecs_flags16_t f_2nd = flecs_rule_ref_flags(op->flags, EcsRuleSecond);
    ecs_entity_t trav = flecs_get_ref_entity(&op->first, f_1st, ctx);
    ecs_entity_t second = flecs_get_ref_entity(&op->second, f_2nd, ctx);
    ecs_rule_trav_ctx_t *trav_ctx = flecs_op_ctx(ctx, trav);

    if (!redo) {
        ecs_record_t *r_second = flecs_entities_get(ctx->world, second);
        bool traversable = r_second && r_second->row & EcsEntityIsTraversable;
        bool reflexive = op->match_flags & EcsTermReflexive;
        if (!traversable && !reflexive) {
            trav_ctx->cache.id = 0;

            /* If there's no record for the entity, it can't have a subtree so
             * forward operation to a regular select. */
            return flecs_rule_select(op, redo, ctx);
        }

        /* Entity is traversable, which means it could have a subtree */
        flecs_rule_get_down_cache(ctx, &trav_ctx->cache, trav, second);
        trav_ctx->index = 0;

        if (op->match_flags & EcsTermReflexive) {
            trav_ctx->index = -1;
            return flecs_rule_trav_unknown_src_reflexive(
                op, ctx, trav, second);
        }
    } else {
        if (!trav_ctx->cache.id) {
            /* No traversal cache, which means this is a regular select */
            return flecs_rule_select(op, redo, ctx);
        }
    }

    if (trav_ctx->index == -1) {
        redo = false; /* First result after handling reflexive relationship */
        trav_ctx->index = 0;
    }

    /* Forward to select */
    int32_t count = ecs_vec_count(&trav_ctx->cache.entities);
    ecs_trav_elem_t *elems = ecs_vec_first(&trav_ctx->cache.entities);
    for (; trav_ctx->index < count; trav_ctx->index ++) {
        ecs_trav_elem_t *el = &elems[trav_ctx->index];
        trav_ctx->and.idr = el->idr; /* prevents lookup by select */
        if (flecs_rule_select_w_id(op, redo, ctx, ecs_pair(trav, el->entity))) {
            return true;
        }
        
        redo = false;
    }

    return false;
}

static
bool flecs_rule_trav_yield_reflexive_src(
    const ecs_rule_op_t *op,
    const ecs_rule_run_ctx_t *ctx,
    ecs_table_range_t *range,
    ecs_entity_t trav)
{
    ecs_var_t *vars = ctx->vars;
    ecs_rule_trav_ctx_t *trav_ctx = flecs_op_ctx(ctx, trav);
    int32_t offset = trav_ctx->offset, count = trav_ctx->count;
    bool src_is_var = op->flags & (EcsRuleIsVar << EcsRuleSrc);

    if (trav_ctx->index >= (offset + count)) {
        /* Restore previous offset, count */
        if (src_is_var) {
            ecs_var_id_t src_var = op->src.var;
            vars[src_var].range.offset = offset;
            vars[src_var].range.count = count;
            vars[src_var].entity = 0;
        }
        return false;
    }

    ecs_entity_t entity = ecs_vec_get_t(
        &range->table->data.entities, ecs_entity_t, trav_ctx->index)[0];
    flecs_rule_set_trav_match(op, -1, trav, entity, ctx);

    /* Hijack existing variable to return one result at a time */
    if (src_is_var) {
        ecs_var_id_t src_var = op->src.var;
        ecs_table_t *table = vars[src_var].range.table;
        ecs_assert(!table || table == ecs_get_table(ctx->world, entity),
            ECS_INTERNAL_ERROR, NULL);
        (void)table;
        vars[src_var].entity = entity;
        vars[src_var].range = flecs_range_from_entity(entity, ctx);
    }

    return true;
}

static
bool flecs_rule_trav_fixed_src_up_unknown_second(
    const ecs_rule_op_t *op,
    bool redo,
    const ecs_rule_run_ctx_t *ctx)
{
    ecs_flags16_t f_1st = flecs_rule_ref_flags(op->flags, EcsRuleFirst);
    ecs_flags16_t f_src = flecs_rule_ref_flags(op->flags, EcsRuleSrc);
    ecs_entity_t trav = flecs_get_ref_entity(&op->first, f_1st, ctx);
    ecs_table_range_t range = flecs_get_ref_range(&op->src, f_src, ctx);
    ecs_table_t *table = range.table;
    ecs_rule_trav_ctx_t *trav_ctx = flecs_op_ctx(ctx, trav);

    if (!redo) {
        flecs_rule_get_up_cache(ctx, &trav_ctx->cache, trav, table);
        trav_ctx->index = 0;
        if (op->match_flags & EcsTermReflexive) {
            trav_ctx->yield_reflexive = true;
            trav_ctx->index = range.offset;
            trav_ctx->offset = range.offset;
            trav_ctx->count = range.count ? range.count : ecs_table_count(table);
        }
    } else {
        trav_ctx->index ++;
    }

    if (trav_ctx->yield_reflexive) {
        if (flecs_rule_trav_yield_reflexive_src(op, ctx, &range, trav)) {
            return true;
        }
        trav_ctx->yield_reflexive = false;
        trav_ctx->index = 0;
    }

    if (trav_ctx->index >= ecs_vec_count(&trav_ctx->cache.entities)) {
        return false;
    }

    ecs_trav_elem_t *el = ecs_vec_get_t(
        &trav_ctx->cache.entities, ecs_trav_elem_t, trav_ctx->index);
    flecs_rule_set_trav_match(op, el->column, trav, el->entity, ctx);
    return true;
}

static
bool flecs_rule_trav(
    const ecs_rule_op_t *op,
    bool redo,
    const ecs_rule_run_ctx_t *ctx)
{
    uint64_t written = ctx->written[ctx->op_index];

    if (!flecs_ref_is_written(op, &op->src, EcsRuleSrc, written)) {
        if (!flecs_ref_is_written(op, &op->second, EcsRuleSecond, written)) {
            /* This can't happen, src or second should have been resolved */
            ecs_abort(ECS_INTERNAL_ERROR, 
                "invalid instruction sequence: unconstrained traversal");
            return false;
        } else {
            return flecs_rule_trav_unknown_src_up_fixed_second(op, redo, ctx);
        }
    } else {
        if (!flecs_ref_is_written(op, &op->second, EcsRuleSecond, written)) {
            return flecs_rule_trav_fixed_src_up_unknown_second(op, redo, ctx);
        } else {
            return flecs_rule_trav_fixed_src_up_fixed_second(op, redo, ctx);
        }
    }
}

static
bool flecs_rule_idsright(
    const ecs_rule_op_t *op,
    bool redo,
    const ecs_rule_run_ctx_t *ctx)
{
    ecs_rule_ids_ctx_t *op_ctx = flecs_op_ctx(ctx, ids);
    ecs_id_record_t *cur;

    if (!redo) {
        ecs_id_t id = flecs_rule_op_get_id(op, ctx);
        if (!ecs_id_is_wildcard(id)) {
            /* If id is not a wildcard, we can directly return it. This can 
             * happen if a variable was constrained by an iterator. */
            op_ctx->cur = NULL;
            flecs_rule_set_vars(op, id, ctx);
            return true;
        }

        cur = op_ctx->cur = flecs_id_record_get(ctx->world, id);
        if (!cur) {
            return false;
        }

        cur = op_ctx->cur = cur->first.next;
    } else {
        if (!op_ctx->cur) {
            return false;
        }

        cur = op_ctx->cur = op_ctx->cur->first.next;
    }

    if (!cur) {
        return false;
    }

    flecs_rule_set_vars(op, cur->id, ctx);

    if (op->field_index != -1) {
        ecs_iter_t *it = ctx->it;
        ecs_id_t id = flecs_rule_op_get_id_w_written(op, op->written, ctx);
        it->ids[op->field_index] = id;
    }

    return true;
}

static
bool flecs_rule_idsleft(
    const ecs_rule_op_t *op,
    bool redo,
    const ecs_rule_run_ctx_t *ctx)
{
    ecs_rule_ids_ctx_t *op_ctx = flecs_op_ctx(ctx, ids);
    ecs_id_record_t *cur;

    if (!redo) {
        ecs_id_t id = flecs_rule_op_get_id(op, ctx);
        if (!ecs_id_is_wildcard(id)) {
            /* If id is not a wildcard, we can directly return it. This can 
             * happen if a variable was constrained by an iterator. */
            op_ctx->cur = NULL;
            flecs_rule_set_vars(op, id, ctx);
            return true;
        }

        cur = op_ctx->cur = flecs_id_record_get(ctx->world, id);
        if (!cur) {
            return false;
        }

        cur = op_ctx->cur = cur->second.next;
    } else {
        if (!op_ctx->cur) {
            return false;
        }

        cur = op_ctx->cur = op_ctx->cur->second.next;
    }

    if (!cur) {
        return false;
    }

    flecs_rule_set_vars(op, cur->id, ctx);

    if (op->field_index != -1) {
        ecs_iter_t *it = ctx->it;
        ecs_id_t id = flecs_rule_op_get_id_w_written(op, op->written, ctx);
        it->ids[op->field_index] = id;
    }

    return true;
}

static
bool flecs_rule_each(
    const ecs_rule_op_t *op,
    bool redo,
    const ecs_rule_run_ctx_t *ctx)
{
    ecs_rule_each_ctx_t *op_ctx = flecs_op_ctx(ctx, each);
    int32_t row;

    ecs_table_range_t range = flecs_rule_var_get_range(op->first.var, ctx);
    ecs_table_t *table = range.table;
    if (!table) {
        return false;
    }

    if (!redo) {
        row = op_ctx->row = range.offset;
    } else {
        int32_t end = range.count;
        if (end) {
            end += range.offset;
        } else {
            end = table->data.entities.count;
        }
        row = ++ op_ctx->row;
        if (op_ctx->row >= end) {
            return false;
        }
    }

    ecs_assert(row < ecs_table_count(table), ECS_INTERNAL_ERROR, NULL);

    ecs_entity_t *entities = table->data.entities.array;
    ecs_entity_t e;
    do {
        e = entities[row ++];
    
        /* Exclude entities that are used as markers by rule engine */
    } while ((e == EcsWildcard) || (e == EcsAny) || 
        (e == EcsThis) || (e == EcsVariable));

    flecs_rule_var_set_entity(op, op->src.var, e, ctx);

    return true;
}

static
bool flecs_rule_store(
    const ecs_rule_op_t *op,
    bool redo,
    const ecs_rule_run_ctx_t *ctx)
{
    if (!redo) {
        flecs_rule_var_set_entity(op, op->src.var, op->first.entity, ctx);
        return true;
    } else {
        return false;
    }
}

static
bool flecs_rule_union(
    const ecs_rule_op_t *op,
    bool redo,
    ecs_rule_run_ctx_t *ctx)
{
    if (!redo) {
        ctx->jump = flecs_itolbl(ctx->op_index + 1);
        return true;
    } else {
        ecs_rule_lbl_t next = flecs_itolbl(ctx->prev_index + 1);
        if (next == op->next) {
            return false;
        }
        ctx->jump = next;
        return true;
    }
}

static
bool flecs_rule_end(
    const ecs_rule_op_t *op,
    bool redo,
    ecs_rule_run_ctx_t *ctx)
{
    (void)op;

    ecs_rule_ctrlflow_ctx_t *op_ctx = flecs_op_ctx(ctx, ctrlflow);
    if (!redo) {
        op_ctx->lbl = ctx->prev_index;
        return true;
    } else {
        ctx->jump = op_ctx->lbl;
        return true;
    }
}

static
bool flecs_rule_not(
    const ecs_rule_op_t *op,
    bool redo,
    ecs_rule_run_ctx_t *ctx)
{
    if (redo) {
        return false;
    }

    int32_t field = op->field_index;
    if (field == -1) {
        return true;
    }

    ecs_iter_t *it = ctx->it;

    /* Not terms return no data */
    it->columns[field] = 0;

    /* Ignore variables written by Not operation */
    uint64_t *written = ctx->written;
    uint64_t written_cur = written[ctx->op_index] = written[op->prev + 1];
    ecs_flags16_t flags_1st = flecs_rule_ref_flags(op->flags, EcsRuleFirst);
    ecs_flags16_t flags_2nd = flecs_rule_ref_flags(op->flags, EcsRuleSecond);

    /* Overwrite id with cleared out variables */
    ecs_id_t id = flecs_rule_op_get_id(op, ctx);
    if (id) {
        it->ids[field] = id;
    }

    /* Reset variables */
    if (flags_1st & EcsRuleIsVar) {
        if (!flecs_ref_is_written(op, &op->first, EcsRuleFirst, written_cur)){
            flecs_rule_var_reset(op->first.var, ctx);
        }
    }
    if (flags_2nd & EcsRuleIsVar) {
        if (!flecs_ref_is_written(op, &op->second, EcsRuleSecond, written_cur)){
            flecs_rule_var_reset(op->second.var, ctx);
        }
    }

    /* If term has entity src, set it because no other instruction might */
    if (op->flags & (EcsRuleIsEntity << EcsRuleSrc)) {
        it->sources[field] = op->src.entity;
    }

    return true; /* Flip result */
}

static
const char* flecs_rule_name_arg(
    const ecs_rule_op_t *op,
    ecs_rule_run_ctx_t *ctx)
{
    int8_t term_index = op->term_index;
    ecs_term_t *term = &ctx->rule->filter.terms[term_index];
    return term->second.name;
}

static
bool flecs_rule_compare_range(
    const ecs_table_range_t *l,
    const ecs_table_range_t *r)
{
    if (l->table != r->table) {
        return false;
    }

    if (l->count) {
        int32_t l_end = l->offset + l->count;
        int32_t r_end = r->offset + r->count;
        if (r->offset < l->offset) {
            return false;
        }
        if (r_end > l_end) {
            return false;
        }
    } else {
        /* Entire table is matched */
    }

    return true;
}

static
bool flecs_rule_pred_eq_w_range(
    const ecs_rule_op_t *op,
    bool redo,
    ecs_rule_run_ctx_t *ctx,
    ecs_table_range_t r)
{
    if (redo) {
        return false;
    }

    uint64_t written = ctx->written[ctx->op_index];
    ecs_var_id_t first_var = op->first.var;
    if (!(written & (1ull << first_var))) {
        /* left = unknown, right = known. Assign right-hand value to left */
        ecs_var_id_t l = first_var;
        ctx->vars[l].range = r;
        return true;
    } else {
        ecs_table_range_t l = flecs_rule_get_range(
            op, &op->first, EcsRuleFirst, ctx);

        if (!flecs_rule_compare_range(&l, &r)) {
            return false;
        }

        ctx->vars[first_var].range.offset = r.offset;
        ctx->vars[first_var].range.count = r.count;
        return true;
    }
}

static
bool flecs_rule_pred_eq(
    const ecs_rule_op_t *op,
    bool redo,
    ecs_rule_run_ctx_t *ctx)
{
    uint64_t written = ctx->written[ctx->op_index]; (void)written;
    ecs_assert(flecs_ref_is_written(op, &op->second, EcsRuleSecond, written),
        ECS_INTERNAL_ERROR, 
            "invalid instruction sequence: uninitialized eq operand");

    ecs_table_range_t r = flecs_rule_get_range(
        op, &op->second, EcsRuleSecond, ctx);
    return flecs_rule_pred_eq_w_range(op, redo, ctx, r);
}

static
bool flecs_rule_pred_eq_name(
    const ecs_rule_op_t *op,
    bool redo,
    ecs_rule_run_ctx_t *ctx)
{
    const char *name = flecs_rule_name_arg(op, ctx);
    ecs_entity_t e = ecs_lookup_fullpath(ctx->world, name);
    if (!e) {
        /* Entity doesn't exist */
        return false;
    }

    ecs_table_range_t r = flecs_range_from_entity(e, ctx);
    return flecs_rule_pred_eq_w_range(op, redo, ctx, r);
}

static
bool flecs_rule_pred_neq_w_range(
    const ecs_rule_op_t *op,
    bool redo,
    ecs_rule_run_ctx_t *ctx,
    ecs_table_range_t r)
{
    ecs_rule_eq_ctx_t *op_ctx = flecs_op_ctx(ctx, eq);
    ecs_var_id_t first_var = op->first.var;
    ecs_table_range_t l = flecs_rule_get_range(
        op, &op->first, EcsRuleFirst, ctx);

    /* If tables don't match, neq always returns once */
    if (l.table != r.table) {
        return true && !redo;
    }

    int32_t l_offset;
    int32_t l_count;
    if (!redo) {
        /* Make sure we're working with the correct table count */
        if (!l.count && l.table) {
            l.count = ecs_table_count(l.table);
        }

        l_offset = l.offset;
        l_count = l.count;

        /* Cache old value */
        op_ctx->range = l;
    } else {
        l_offset = op_ctx->range.offset;
        l_count = op_ctx->range.count;
    }

    /* If the table matches, a Neq returns twice: once for the slice before the
     * excluded slice, once for the slice after the excluded slice. If the right
     * hand range starts & overlaps with the left hand range, there is only
     * one slice. */
    ecs_var_t *var = &ctx->vars[first_var];
    if (!redo && r.offset > l_offset) {
        int32_t end = r.offset;
        if (end > l_count) {
            end = l_count;
        }

        /* Return first slice */
        var->range.table = l.table;
        var->range.offset = l_offset;
        var->range.count = end - l_offset;
        op_ctx->redo = false;
        return true;
    } else if (!op_ctx->redo) {
        int32_t l_end = op_ctx->range.offset + l_count;
        int32_t r_end = r.offset + r.count;

        if (l_end <= r_end) {
            /* If end of existing range falls inside the excluded range, there's
             * nothing more to return */
            var->range = l;
            return false;
        }

        /* Return second slice */
        var->range.table = l.table;
        var->range.offset = r_end;
        var->range.count = l_end - r_end;

        /* Flag so we know we're done the next redo */
        op_ctx->redo = true;
        return true;
    } else {
        /* Restore previous value */
        var->range = l;
        return false;
    }
}

static
bool flecs_rule_pred_match(
    const ecs_rule_op_t *op,
    bool redo,
    ecs_rule_run_ctx_t *ctx,
    bool is_neq)
{
    ecs_rule_eq_ctx_t *op_ctx = flecs_op_ctx(ctx, eq);
    uint64_t written = ctx->written[ctx->op_index];
    ecs_assert(flecs_ref_is_written(op, &op->first, EcsRuleFirst, written),
        ECS_INTERNAL_ERROR, 
            "invalid instruction sequence: uninitialized match operand");
    (void)written;

    ecs_var_id_t first_var = op->first.var;
    const char *match = flecs_rule_name_arg(op, ctx);
    ecs_table_range_t l;
    if (!redo) {
        l = flecs_rule_get_range(op, &op->first, EcsRuleFirst, ctx);
        if (!l.table) {
            return false;
        }

        if (!l.count) {
            l.count = ecs_table_count(l.table);
        }

        op_ctx->range = l;
        op_ctx->index = l.offset;
        op_ctx->name_col = flecs_ito(int16_t,   
            ecs_table_get_index(ctx->world, l.table, 
                ecs_pair(ecs_id(EcsIdentifier), EcsName)));
        if (op_ctx->name_col == -1) {
            return is_neq;
        }
        op_ctx->name_col = flecs_ito(int16_t, 
            l.table->storage_map[op_ctx->name_col]);
        ecs_assert(op_ctx->name_col != -1, ECS_INTERNAL_ERROR, NULL);
    } else {
        if (op_ctx->name_col == -1) {
            /* Table has no name */
            return false;
        }

        l = op_ctx->range;
    }

    const EcsIdentifier *names = l.table->data.columns[op_ctx->name_col].array;
    int32_t count = l.offset + l.count, offset = -1;
    for (; op_ctx->index < count; op_ctx->index ++) {
        const char *name = names[op_ctx->index].value;
        bool result = strstr(name, match);
        if (is_neq) {
            result = !result;
        }

        if (!result) {
            if (offset != -1) {
                break;
            }
        } else {
            if (offset == -1) {
                offset = op_ctx->index;
            }
        }
    }

    if (offset == -1) {
        ctx->vars[first_var].range = op_ctx->range;
        return false;
    }

    ctx->vars[first_var].range.offset = offset;
    ctx->vars[first_var].range.count = (op_ctx->index - offset);
    return true;
}

static
bool flecs_rule_pred_eq_match(
    const ecs_rule_op_t *op,
    bool redo,
    ecs_rule_run_ctx_t *ctx)
{
    return flecs_rule_pred_match(op, redo, ctx, false);
}

static
bool flecs_rule_pred_neq_match(
    const ecs_rule_op_t *op,
    bool redo,
    ecs_rule_run_ctx_t *ctx)
{
    return flecs_rule_pred_match(op, redo, ctx, true);
}

static
bool flecs_rule_pred_neq(
    const ecs_rule_op_t *op,
    bool redo,
    ecs_rule_run_ctx_t *ctx)
{
    uint64_t written = ctx->written[ctx->op_index]; (void)written;
    ecs_assert(flecs_ref_is_written(op, &op->second, EcsRuleSecond, written),
        ECS_INTERNAL_ERROR, 
            "invalid instruction sequence: uninitialized neq operand");

    ecs_table_range_t r = flecs_rule_get_range(
        op, &op->second, EcsRuleSecond, ctx);
    return flecs_rule_pred_neq_w_range(op, redo, ctx, r);
}

static
bool flecs_rule_pred_neq_name(
    const ecs_rule_op_t *op,
    bool redo,
    ecs_rule_run_ctx_t *ctx)
{
    const char *name = flecs_rule_name_arg(op, ctx);
    ecs_entity_t e = ecs_lookup_fullpath(ctx->world, name);
    if (!e) {
        /* Entity doesn't exist */
        return true && !redo;
    }

    ecs_table_range_t r = flecs_range_from_entity(e, ctx);
    return flecs_rule_pred_neq_w_range(op, redo, ctx, r);
}

static
bool flecs_rule_setvars(
    const ecs_rule_op_t *op,
    bool redo,
    ecs_rule_run_ctx_t *ctx)
{
    (void)op;

    const ecs_rule_t *rule = ctx->rule;
    const ecs_filter_t *filter = &rule->filter;
    ecs_var_id_t *src_vars = rule->src_vars;
    ecs_iter_t *it = ctx->it;

    if (redo) {
        return false;
    }

    int32_t i;
    for (i = 0; i < filter->field_count; i ++) {
        ecs_var_id_t var_id = src_vars[i];
        if (!var_id) {
            continue;
        }

        it->sources[i] = flecs_rule_var_get_entity(var_id, ctx);

        int32_t column = it->columns[i];
        if (column > 0) {
            it->columns[i] = -column;
        }
    }

    return true;
}

static
bool flecs_rule_setthis(
    const ecs_rule_op_t *op,
    bool redo,
    ecs_rule_run_ctx_t *ctx)
{
    ecs_rule_setthis_ctx_t *op_ctx = flecs_op_ctx(ctx, setthis);
    ecs_var_t *vars = ctx->vars;
    ecs_var_t *this_var = &vars[op->first.var];

    if (!redo) {
        /* Save values so we can restore them later */
        op_ctx->range = vars[0].range;

        /* Constrain This table variable to a single entity from the table */
        vars[0].range = flecs_range_from_entity(this_var->entity, ctx);
        vars[0].entity = this_var->entity;
        return true;
    } else {
        /* Restore previous values, so that instructions that are operating on
         * the table variable use all the entities in the table. */
        vars[0].range = op_ctx->range;
        vars[0].entity = 0;
        return false;
    }
}

static
bool flecs_rule_setfixed(
    const ecs_rule_op_t *op,
    bool redo,
    ecs_rule_run_ctx_t *ctx)
{
    (void)op;
    const ecs_rule_t *rule = ctx->rule;
    const ecs_filter_t *filter = &rule->filter;
    ecs_iter_t *it = ctx->it;

    if (redo) {
        return false;
    }

    int32_t i;
    for (i = 0; i < filter->term_count; i ++) {
        ecs_term_t *term = &filter->terms[i];
        ecs_term_id_t *src = &term->src;
        if (src->flags & EcsIsEntity) {
            it->sources[term->field_index] = src->id;
        }
    }

    return true;
}

static
bool flecs_rule_setids(
    const ecs_rule_op_t *op,
    bool redo,
    ecs_rule_run_ctx_t *ctx)
{
    (void)op;
    const ecs_rule_t *rule = ctx->rule;
    const ecs_filter_t *filter = &rule->filter;
    ecs_iter_t *it = ctx->it;

    if (redo) {
        return false;
    }

    int32_t i;
    for (i = 0; i < filter->term_count; i ++) {
        ecs_term_t *term = &filter->terms[i];
        it->ids[term->field_index] = term->id;
    }

    return true;
}

/* Check if entity is stored in table */
static
bool flecs_rule_contain(
    const ecs_rule_op_t *op,
    bool redo,
    ecs_rule_run_ctx_t *ctx)
{
    if (redo) {
        return false;
    }

    ecs_var_id_t src_id = op->src.var;
    ecs_var_id_t first_id = op->first.var;

    ecs_table_t *table = flecs_rule_var_get_table(src_id, ctx);
    ecs_entity_t e = flecs_rule_var_get_entity(first_id, ctx);
    return table == ecs_get_table(ctx->world, e);
}

/* Check if first and second id of pair from last operation are the same */
static
bool flecs_rule_pair_eq(
    const ecs_rule_op_t *op,
    bool redo,
    ecs_rule_run_ctx_t *ctx)
{
    if (redo) {
        return false;
    }

    ecs_iter_t *it = ctx->it;
    ecs_id_t id = it->ids[op->field_index];
    return ECS_PAIR_FIRST(id) == ECS_PAIR_SECOND(id);
}

static
bool flecs_rule_jmp_if_not(
    const ecs_rule_op_t *op,
    bool redo,
    ecs_rule_run_ctx_t *ctx)
{
    if (!redo) {
        flecs_op_ctx(ctx, cond)->cond = false;
        return true;
    } else {
        if (!flecs_op_ctx(ctx, cond)->cond) {
            ctx->jump = op->other;
        }
        return false;
    }
}

static
bool flecs_rule_jmp_set_cond(
    const ecs_rule_op_t *op,
    bool redo,
    ecs_rule_run_ctx_t *ctx)
{
    if (!redo) {
        ctx->op_ctx[op->other].is.cond.cond = true;
        return true;
    } else {
        return false;
    }
}

static
bool flecs_rule_jmp_not_set(
    const ecs_rule_op_t *op,
    bool redo,
    ecs_rule_run_ctx_t *ctx)
{
    if (!redo) {
        ecs_var_t *vars = ctx->vars;
        if (flecs_rule_ref_flags(op->flags, EcsRuleFirst) == EcsRuleIsVar) {
            if (vars[op->first.var].entity == EcsWildcard) {
                ctx->jump = op->other;
                return true;
            }
        }
        if (flecs_rule_ref_flags(op->flags, EcsRuleSecond) == EcsRuleIsVar) {
            if (vars[op->second.var].entity == EcsWildcard) {
                ctx->jump = op->other;
                return true;
            }
        }
        if (flecs_rule_ref_flags(op->flags, EcsRuleSrc) == EcsRuleIsVar) {
            if (vars[op->src.var].entity == EcsWildcard) {
                ctx->jump = op->other;
                return true;
            }
        }

        return true;
    } else {
        return false;
    }
}

static
bool flecs_rule_run(
    const ecs_rule_op_t *op,
    bool redo,
    ecs_rule_run_ctx_t *ctx)
{
    switch(op->kind) {
    case EcsRuleAnd: return flecs_rule_and(op, redo, ctx);
    case EcsRuleAndId: return flecs_rule_and_id(op, redo, ctx);
    case EcsRuleAndAny: return flecs_rule_and_any(op, redo, ctx);
    case EcsRuleWith: return flecs_rule_with(op, redo, ctx);
    case EcsRuleTrav: return flecs_rule_trav(op, redo, ctx);
    case EcsRuleIdsRight: return flecs_rule_idsright(op, redo, ctx);
    case EcsRuleIdsLeft: return flecs_rule_idsleft(op, redo, ctx);
    case EcsRuleEach: return flecs_rule_each(op, redo, ctx);
    case EcsRuleStore: return flecs_rule_store(op, redo, ctx);
    case EcsRuleUnion: return flecs_rule_union(op, redo, ctx);
    case EcsRuleEnd: return flecs_rule_end(op, redo, ctx);
    case EcsRuleNot: return flecs_rule_not(op, redo, ctx);
    case EcsRulePredEq: return flecs_rule_pred_eq(op, redo, ctx);
    case EcsRulePredNeq: return flecs_rule_pred_neq(op, redo, ctx);
    case EcsRulePredEqName: return flecs_rule_pred_eq_name(op, redo, ctx);
    case EcsRulePredNeqName: return flecs_rule_pred_neq_name(op, redo, ctx);
    case EcsRulePredEqMatch: return flecs_rule_pred_eq_match(op, redo, ctx);
    case EcsRulePredNeqMatch: return flecs_rule_pred_neq_match(op, redo, ctx);
    case EcsRuleSetVars: return flecs_rule_setvars(op, redo, ctx);
    case EcsRuleSetThis: return flecs_rule_setthis(op, redo, ctx);
    case EcsRuleSetFixed: return flecs_rule_setfixed(op, redo, ctx);
    case EcsRuleSetIds: return flecs_rule_setids(op, redo, ctx);
    case EcsRuleContain: return flecs_rule_contain(op, redo, ctx);
    case EcsRulePairEq: return flecs_rule_pair_eq(op, redo, ctx);
    case EcsRuleJmpCondFalse: return flecs_rule_jmp_if_not(op, redo, ctx);
    case EcsRuleSetCond: return flecs_rule_jmp_set_cond(op, redo, ctx);
    case EcsRuleJmpNotSet: return flecs_rule_jmp_not_set(op, redo, ctx);
    case EcsRuleYield: return false;
    case EcsRuleNothing: return false;
    }
    return false;
}

static
void flecs_rule_iter_init(
    ecs_rule_run_ctx_t *ctx)
{
    ecs_iter_t *it = ctx->it;
    if (ctx->written) {
        const ecs_rule_t *rule = ctx->rule;
        ecs_flags64_t it_written = it->constrained_vars;
        ctx->written[0] = it_written;
        if (it_written && ctx->rule->src_vars) {
            /* If variables were constrained, check if there are any table
             * variables that have a constrained entity variable. */
            ecs_var_t *vars = ctx->vars;
            int32_t i, count = rule->filter.field_count;
            for (i = 0; i < count; i ++) {
                ecs_var_id_t var_id = rule->src_vars[i];
                ecs_rule_var_t *var = &rule->vars[var_id];

                if (!(it_written & (1ull << var_id)) || 
                    (var->kind == EcsVarTable) || (var->table_id == EcsVarNone)) 
                {
                    continue;
                }

                /* Initialize table variable with constrained entity variable */
                ecs_var_t *tvar = &vars[var->table_id];
                tvar->range = flecs_range_from_entity(vars[var_id].entity, ctx);
                ctx->written[0] |= (1ull << var->table_id); /* Mark as written */
            }
        }
    }

    flecs_iter_validate(it);
}

bool ecs_rule_next(
    ecs_iter_t *it)
{
    ecs_check(it != NULL, ECS_INVALID_PARAMETER, NULL);
    ecs_check(it->next == ecs_rule_next, ECS_INVALID_PARAMETER, NULL);

    if (flecs_iter_next_row(it)) {
        return true;
    }

    return flecs_iter_next_instanced(it, ecs_rule_next_instanced(it));
error:
    return false;
}

bool ecs_rule_next_instanced(
    ecs_iter_t *it)
{
    ecs_assert(it != NULL, ECS_INVALID_PARAMETER, NULL);
    ecs_assert(it->next == ecs_rule_next, ECS_INVALID_PARAMETER, NULL);

    ecs_rule_iter_t *rit = &it->priv.iter.rule;
    bool redo = it->flags & EcsIterIsValid;
    ecs_rule_lbl_t next;

    ecs_rule_run_ctx_t ctx;
    ctx.world = it->real_world;
    ctx.rule = rit->rule;
    ctx.it = it;
    ctx.vars = rit->vars;
    ctx.rule_vars = rit->rule_vars;
    ctx.written = rit->written;
    ctx.prev_index = -1;
    ctx.jump = -1;
    ctx.op_ctx = rit->op_ctx;
    const ecs_rule_op_t *ops = rit->ops;

    if (!(it->flags & EcsIterIsValid)) {
        if (!ctx.rule) {
            goto done;
        }
        flecs_rule_iter_init(&ctx);
    }

    do {
        ctx.op_index = rit->op;
        const ecs_rule_op_t *op = &ops[ctx.op_index];

#ifdef FLECS_DEBUG
        rit->profile[ctx.op_index].count[redo] ++;
#endif

        bool result = flecs_rule_run(op, redo, &ctx);
        ctx.prev_index = ctx.op_index;

        next = (&op->prev)[result];
        if (ctx.jump != -1) {
            next = ctx.jump;
            ctx.jump = -1;
        }

        if ((next > ctx.op_index)) {
            ctx.written[next] |= ctx.written[ctx.op_index] | op->written;
        }

        redo = next < ctx.prev_index;
        rit->op = next;

        if (op->kind == EcsRuleYield) {
            ecs_table_range_t *range = &rit->vars[0].range;
            ecs_table_t *table = range->table;
            if (table && !range->count) {
                range->count = ecs_table_count(table);
            }
            flecs_iter_populate_data(ctx.world, it, range->table, range->offset,
                range->count, it->ptrs);
            return true;
        }
    } while (next >= 0);

done:
    ecs_iter_fini(it);
    return false;
}

static
void flecs_rule_iter_fini_ctx(
    ecs_iter_t *it,
    ecs_rule_iter_t *rit)
{
    const ecs_rule_t *rule = rit->rule;
    int32_t i, count = rule->op_count;
    ecs_rule_op_t *ops = rule->ops;
    ecs_rule_op_ctx_t *ctx = rit->op_ctx;
    ecs_allocator_t *a = flecs_rule_get_allocator(it);

    for (i = 0; i < count; i ++) {
        ecs_rule_op_t *op = &ops[i];
        switch(op->kind) {
        case EcsRuleTrav:
            flecs_rule_trav_cache_fini(a, &ctx[i].is.trav.cache);
            break;
        default:
            break;
        }
    }
}

static
void flecs_rule_iter_fini(
    ecs_iter_t *it)
{
    ecs_rule_iter_t *rit = &it->priv.iter.rule;
    ecs_assert(rit->rule != NULL, ECS_INVALID_OPERATION, NULL);
    ecs_poly_assert(rit->rule, ecs_rule_t);
    int32_t op_count = rit->rule->op_count;
    int32_t var_count = rit->rule->var_count;

#ifdef FLECS_DEBUG
    if (it->flags & EcsIterProfile) {
        char *str = ecs_rule_str_w_profile(rit->rule, it);
        printf("%s\n", str);
        ecs_os_free(str);
    }

    flecs_iter_free_n(rit->profile, ecs_rule_op_profile_t, op_count);
#endif

    flecs_rule_iter_fini_ctx(it, rit);
    flecs_iter_free_n(rit->vars, ecs_var_t, var_count);
    flecs_iter_free_n(rit->written, ecs_write_flags_t, op_count);
    flecs_iter_free_n(rit->op_ctx, ecs_rule_op_ctx_t, op_count);
    rit->vars = NULL;
    rit->written = NULL;
    rit->op_ctx = NULL;
    rit->rule = NULL;
}

ecs_iter_t ecs_rule_iter(
    const ecs_world_t *world,
    const ecs_rule_t *rule)
{
    ecs_iter_t it = {0};
    ecs_rule_iter_t *rit = &it.priv.iter.rule;
    ecs_check(rule != NULL, ECS_INVALID_PARAMETER, NULL);

    ecs_run_aperiodic(rule->filter.world, EcsAperiodicEmptyTables);

    int32_t i, var_count = rule->var_count, op_count = rule->op_count;
    it.world = (ecs_world_t*)world;
    it.real_world = rule->filter.world;
    it.terms = rule->filter.terms;
    it.next = ecs_rule_next;
    it.fini = flecs_rule_iter_fini;
    it.field_count = rule->filter.field_count;
    it.sizes = rule->filter.sizes;
    flecs_filter_apply_iter_flags(&it, &rule->filter);

    flecs_iter_init(world, &it, 
        flecs_iter_cache_ids |
        flecs_iter_cache_columns |
        flecs_iter_cache_sources |
        flecs_iter_cache_ptrs);

    rit->rule = rule;
    rit->rule_vars = rule->vars;
    rit->ops = rule->ops;
    if (var_count) {
        rit->vars = flecs_iter_calloc_n(&it, ecs_var_t, var_count);
    }
    if (op_count) {
        rit->written = flecs_iter_calloc_n(&it, ecs_write_flags_t, op_count);
        rit->op_ctx = flecs_iter_calloc_n(&it, ecs_rule_op_ctx_t, op_count);
    }

#ifdef FLECS_DEBUG
    rit->profile = flecs_iter_calloc_n(&it, ecs_rule_op_profile_t, op_count);
#endif

    for (i = 0; i < var_count; i ++) {
        rit->vars[i].entity = EcsWildcard;
    }

    it.variables = rit->vars;
    it.variable_count = rule->var_pub_count;
    it.variable_names = rule->var_names;

error:
    return it;
}

#endif

/**
 * @file addons/doc.c
 * @brief Doc addon.
 */


#ifdef FLECS_DOC

static ECS_COPY(EcsDocDescription, dst, src, {
    ecs_os_strset((char**)&dst->value, src->value);

})

static ECS_MOVE(EcsDocDescription, dst, src, {
    ecs_os_free((char*)dst->value);
    dst->value = src->value;
    src->value = NULL;
})

static ECS_DTOR(EcsDocDescription, ptr, { 
    ecs_os_free((char*)ptr->value);
})

void ecs_doc_set_name(
    ecs_world_t *world,
    ecs_entity_t entity,
    const char *name)
{
    ecs_set_pair(world, entity, EcsDocDescription, EcsName, {
        .value = (char*)name
    });
}

void ecs_doc_set_brief(
    ecs_world_t *world,
    ecs_entity_t entity,
    const char *description)
{
    ecs_set_pair(world, entity, EcsDocDescription, EcsDocBrief, {
        .value = (char*)description
    });
}

void ecs_doc_set_detail(
    ecs_world_t *world,
    ecs_entity_t entity,
    const char *description)
{
    ecs_set_pair(world, entity, EcsDocDescription, EcsDocDetail, {
        .value = (char*)description
    });
}

void ecs_doc_set_link(
    ecs_world_t *world,
    ecs_entity_t entity,
    const char *link)
{
    ecs_set_pair(world, entity, EcsDocDescription, EcsDocLink, {
        .value = (char*)link
    });
}

void ecs_doc_set_color(
    ecs_world_t *world,
    ecs_entity_t entity,
    const char *color)
{
    ecs_set_pair(world, entity, EcsDocDescription, EcsDocColor, {
        .value = (char*)color
    });
}

const char* ecs_doc_get_name(
    const ecs_world_t *world,
    ecs_entity_t entity)
{
    const EcsDocDescription *ptr = ecs_get_pair(
        world, entity, EcsDocDescription, EcsName);
    if (ptr) {
        return ptr->value;
    } else {
        return ecs_get_name(world, entity);
    }
}

const char* ecs_doc_get_brief(
    const ecs_world_t *world,
    ecs_entity_t entity)
{
    const EcsDocDescription *ptr = ecs_get_pair(
        world, entity, EcsDocDescription, EcsDocBrief);
    if (ptr) {
        return ptr->value;
    } else {
        return NULL;
    }
}

const char* ecs_doc_get_detail(
    const ecs_world_t *world,
    ecs_entity_t entity)
{
    const EcsDocDescription *ptr = ecs_get_pair(
        world, entity, EcsDocDescription, EcsDocDetail);
    if (ptr) {
        return ptr->value;
    } else {
        return NULL;
    }
}

const char* ecs_doc_get_link(
    const ecs_world_t *world,
    ecs_entity_t entity)
{
    const EcsDocDescription *ptr = ecs_get_pair(
        world, entity, EcsDocDescription, EcsDocLink);
    if (ptr) {
        return ptr->value;
    } else {
        return NULL;
    }
}

const char* ecs_doc_get_color(
    const ecs_world_t *world,
    ecs_entity_t entity)
{
    const EcsDocDescription *ptr = ecs_get_pair(
        world, entity, EcsDocDescription, EcsDocColor);
    if (ptr) {
        return ptr->value;
    } else {
        return NULL;
    }
}

void FlecsDocImport(
    ecs_world_t *world)
{    
    ECS_MODULE(world, FlecsDoc);

    ecs_set_name_prefix(world, "EcsDoc");

    flecs_bootstrap_component(world, EcsDocDescription);
    flecs_bootstrap_tag(world, EcsDocBrief);
    flecs_bootstrap_tag(world, EcsDocDetail);
    flecs_bootstrap_tag(world, EcsDocLink);
    flecs_bootstrap_tag(world, EcsDocColor);

    ecs_set_hooks(world, EcsDocDescription, { 
        .ctor = ecs_default_ctor,
        .move = ecs_move(EcsDocDescription),
        .copy = ecs_copy(EcsDocDescription),
        .dtor = ecs_dtor(EcsDocDescription)
    });

    ecs_add_id(world, ecs_id(EcsDocDescription), EcsDontInherit);
}

#endif

/**
 * @file addons/parser.c
 * @brief Parser addon.
 */


#ifdef FLECS_PARSER

#include <ctype.h>

#define ECS_ANNOTATION_LENGTH_MAX (16)

#define TOK_NEWLINE '\n'
#define TOK_COLON ':'
#define TOK_AND ','
#define TOK_OR "||"
#define TOK_NOT '!'
#define TOK_OPTIONAL '?'
#define TOK_BITWISE_OR '|'
#define TOK_BRACKET_OPEN '['
#define TOK_BRACKET_CLOSE ']'
#define TOK_SCOPE_OPEN '{'
#define TOK_SCOPE_CLOSE '}'
#define TOK_WILDCARD '*'
#define TOK_VARIABLE '$'
#define TOK_PAREN_OPEN '('
#define TOK_PAREN_CLOSE ')'
#define TOK_EQ "=="
#define TOK_NEQ "!="
#define TOK_MATCH "~="
#define TOK_EXPR_STRING '"'

#define TOK_SELF "self"
#define TOK_UP "up"
#define TOK_DOWN "down"
#define TOK_CASCADE "cascade"
#define TOK_PARENT "parent"

#define TOK_OVERRIDE "OVERRIDE"
#define TOK_ROLE_AND "AND"
#define TOK_ROLE_OR "OR"
#define TOK_ROLE_NOT "NOT"
#define TOK_ROLE_TOGGLE "TOGGLE"

#define TOK_IN "in"
#define TOK_OUT "out"
#define TOK_INOUT "inout"
#define TOK_INOUT_NONE "none"

static
const ecs_id_t ECS_OR =                                            (1ull << 59);

static
const ecs_id_t ECS_NOT =                                           (1ull << 58);

#define ECS_MAX_TOKEN_SIZE (256)

typedef char ecs_token_t[ECS_MAX_TOKEN_SIZE];

const char* ecs_parse_ws_eol(
    const char *ptr)
{
    while (isspace(*ptr)) {
        ptr ++;
    }

    return ptr;
}

const char* ecs_parse_ws(
    const char *ptr)
{
    while ((*ptr != '\n') && isspace(*ptr)) {
        ptr ++;
    }

    return ptr;
}

const char* ecs_parse_digit(
    const char *ptr,
    char *token)
{
    char *tptr = token;
    char ch = ptr[0];

    if (!isdigit(ch) && ch != '-') {
        ecs_parser_error(NULL, NULL, 0, "invalid start of number '%s'", ptr);
        return NULL;
    }

    tptr[0] = ch;
    tptr ++;
    ptr ++;

    for (; (ch = *ptr); ptr ++) {
        if (!isdigit(ch) && (ch != '.') && (ch != 'e')) {
            break;
        }

        tptr[0] = ch;
        tptr ++;
    }

    tptr[0] = '\0';
    
    return ptr;
}

/* -- Private functions -- */

bool flecs_isident(
    char ch)
{
    return isalpha(ch) || (ch == '_');
}

static
bool flecs_valid_identifier_start_char(
    char ch)
{
    if (ch && (flecs_isident(ch) || (ch == '*') ||
        (ch == '0') || (ch == TOK_VARIABLE) || isdigit(ch))) 
    {
        return true;
    }

    return false;
}

static
bool flecs_valid_token_start_char(
    char ch)
{
    if ((ch == '"') || (ch == '{') || (ch == '}') || (ch == ',') || (ch == '-')
        || (ch == '[') || (ch == ']') || (ch == '`') || 
        flecs_valid_identifier_start_char(ch))
    {
        return true;
    }

    return false;
}

static
bool flecs_valid_token_char(
    char ch)
{
    if (ch && (flecs_isident(ch) || isdigit(ch) || ch == '.' || ch == '"')) {
        return true;
    }

    return false;
}

static
bool flecs_valid_operator_char(
    char ch)
{
    if (ch == TOK_OPTIONAL || ch == TOK_NOT) {
        return true;
    }

    return false;
}

const char* ecs_parse_token(
    const char *name,
    const char *expr,
    const char *ptr,
    char *token_out,
    char delim)
{
    int64_t column = ptr - expr;

    ptr = ecs_parse_ws(ptr);
    char *tptr = token_out, ch = ptr[0];

    if (!flecs_valid_token_start_char(ch)) {
        if (ch == '\0' || ch == '\n') {
            ecs_parser_error(name, expr, column, 
                "unexpected end of expression");
        } else {
            ecs_parser_error(name, expr, column, 
                "invalid start of token '%s'", ptr);
        }
        return NULL;
    }

    tptr[0] = ch;
    tptr ++;
    ptr ++;

    if (ch == '{' || ch == '}' || ch == '[' || ch == ']' || ch == ',' || ch == '`') {
        tptr[0] = 0;
        return ptr;
    }

    int tmpl_nesting = 0;
    bool in_str = ch == '"';

    for (; (ch = *ptr); ptr ++) {
        if (ch == '<') {
            tmpl_nesting ++;
        } else if (ch == '>') {
            if (!tmpl_nesting) {
                break;
            }
            tmpl_nesting --;
        } else if (ch == '"') {
            in_str = !in_str;
        } else
        if (!flecs_valid_token_char(ch) && !in_str) {
            break;
        }
        if (delim && (ch == delim)) {
            break;
        }

        tptr[0] = ch;
        tptr ++;
    }

    tptr[0] = '\0';

    if (tmpl_nesting != 0) {
        ecs_parser_error(name, expr, column, 
            "identifier '%s' has mismatching < > pairs", ptr);
        return NULL;
    }

    const char *next_ptr = ecs_parse_ws(ptr);
    if (next_ptr[0] == ':' && next_ptr != ptr) {
        /* Whitespace between token and : is significant */
        ptr = next_ptr - 1;
    } else {
        ptr = next_ptr;
    }

    return ptr;
}

const char* ecs_parse_identifier(
    const char *name,
    const char *expr,
    const char *ptr,
    char *token_out)
{
    if (!flecs_valid_identifier_start_char(ptr[0]) && (ptr[0] != '"')) {
        ecs_parser_error(name, expr, (ptr - expr), 
            "expected start of identifier");
        return NULL;
    }

    ptr = ecs_parse_token(name, expr, ptr, token_out, 0);

    return ptr;
}

static
int flecs_parse_identifier(
    const char *token,
    ecs_term_id_t *out)
{
    const char *tptr = token;
    if (tptr[0] == TOK_VARIABLE && tptr[1]) {
        out->flags |= EcsIsVariable;
        tptr ++;
    }
    if (tptr[0] == TOK_EXPR_STRING && tptr[1]) {
        out->flags |= EcsIsName;
        tptr ++;
        if (tptr[0] == TOK_NOT) {
            /* Already parsed */
            tptr ++;
        }
    }

    out->name = ecs_os_strdup(tptr);

    ecs_size_t len = ecs_os_strlen(out->name);
    if (out->flags & EcsIsName) {
        if (out->name[len - 1] != TOK_EXPR_STRING) {
            ecs_parser_error(NULL, token, 0, "missing '\"' at end of string");
            return -1;
        } else {
            out->name[len - 1] = '\0';
        }
    }

    return 0;
}

static
ecs_entity_t flecs_parse_role(
    const char *name,
    const char *sig,
    int64_t column,
    const char *token)
{
    if (!ecs_os_strcmp(token, TOK_ROLE_AND)) {
        return ECS_AND;
    } else if (!ecs_os_strcmp(token, TOK_ROLE_OR)) {
        return ECS_OR;
    } else if (!ecs_os_strcmp(token, TOK_ROLE_NOT)) {
        return ECS_NOT;
    } else if (!ecs_os_strcmp(token, TOK_OVERRIDE)) {
        return ECS_OVERRIDE;
    } else if (!ecs_os_strcmp(token, TOK_ROLE_TOGGLE)) {
        return ECS_TOGGLE;        
    } else {
        ecs_parser_error(name, sig, column, "invalid role '%s'", token);
        return 0;
    }
}

static
ecs_oper_kind_t flecs_parse_operator(
    char ch)
{
    if (ch == TOK_OPTIONAL) {
        return EcsOptional;
    } else if (ch == TOK_NOT) {
        return EcsNot;
    } else {
        ecs_abort(ECS_INTERNAL_ERROR, NULL);
    }
    return 0;
}

static
const char* flecs_parse_annotation(
    const char *name,
    const char *sig,
    int64_t column,
    const char *ptr, 
    ecs_inout_kind_t *inout_kind_out)
{
    char token[ECS_MAX_TOKEN_SIZE];

    ptr = ecs_parse_identifier(name, sig, ptr, token);
    if (!ptr) {
        return NULL;
    }

    if (!ecs_os_strcmp(token, TOK_IN)) {
        *inout_kind_out = EcsIn;
    } else
    if (!ecs_os_strcmp(token, TOK_OUT)) {
        *inout_kind_out = EcsOut;
    } else
    if (!ecs_os_strcmp(token, TOK_INOUT)) {
        *inout_kind_out = EcsInOut;
    } else if (!ecs_os_strcmp(token, TOK_INOUT_NONE)) {
        *inout_kind_out = EcsInOutNone;
    }

    ptr = ecs_parse_ws(ptr);

    if (ptr[0] != TOK_BRACKET_CLOSE) {
        ecs_parser_error(name, sig, column, "expected ]");
        return NULL;
    }

    return ptr + 1;
}

static
uint8_t flecs_parse_set_token(
    const char *token)
{
    if (!ecs_os_strcmp(token, TOK_SELF)) {
        return EcsSelf;
    } else if (!ecs_os_strcmp(token, TOK_UP)) {
        return EcsUp;
    } else if (!ecs_os_strcmp(token, TOK_DOWN)) {
        return EcsDown;
    } else if (!ecs_os_strcmp(token, TOK_CASCADE)) {
        return EcsCascade;
    } else if (!ecs_os_strcmp(token, TOK_PARENT)) {
        return EcsParent;
    } else {
        return 0;
    }
}

static
const char* flecs_parse_term_flags(
    const ecs_world_t *world,
    const char *name,
    const char *expr,
    int64_t column,
    const char *ptr,
    char *token,
    ecs_term_id_t *id,
    char tok_end)
{
    char token_buf[ECS_MAX_TOKEN_SIZE] = {0};
    if (!token) {
        token = token_buf;
        ptr = ecs_parse_identifier(name, expr, ptr, token);
        if (!ptr) {
            return NULL;
        }
    }

    do {
        uint8_t tok = flecs_parse_set_token(token);
        if (!tok) {
            ecs_parser_error(name, expr, column, 
                "invalid set token '%s'", token);
            return NULL;
        }

        if (id->flags & tok) {
            ecs_parser_error(name, expr, column, 
                "duplicate set token '%s'", token);
            return NULL;            
        }
        
        id->flags |= tok;

        if (ptr[0] == TOK_PAREN_OPEN) {
            ptr ++;

            /* Relationship (overrides IsA default) */
            if (!isdigit(ptr[0]) && flecs_valid_token_start_char(ptr[0])) {
                ptr = ecs_parse_identifier(name, expr, ptr, token);
                if (!ptr) {
                    return NULL;
                }         

                id->trav = ecs_lookup_fullpath(world, token);
                if (!id->trav) {
                    ecs_parser_error(name, expr, column, 
                        "unresolved identifier '%s'", token);
                    return NULL;
                }

                if (ptr[0] == TOK_AND) {
                    ptr = ecs_parse_ws(ptr + 1);
                } else if (ptr[0] != TOK_PAREN_CLOSE) {
                    ecs_parser_error(name, expr, column, 
                        "expected ',' or ')'");
                    return NULL;
                }
            }

            if (ptr[0] != TOK_PAREN_CLOSE) {
                ecs_parser_error(name, expr, column, "expected ')', got '%c'",
                    ptr[0]);
                return NULL;                
            } else {
                ptr = ecs_parse_ws(ptr + 1);
                if (ptr[0] != tok_end && ptr[0] != TOK_AND && ptr[0] != 0) {
                    ecs_parser_error(name, expr, column, 
                        "expected end of set expr");
                    return NULL;
                }
            }
        }

        /* Next token in set expression */
        if (ptr[0] == TOK_BITWISE_OR) {
            ptr ++;
            if (flecs_valid_token_start_char(ptr[0])) {
                ptr = ecs_parse_identifier(name, expr, ptr, token);
                if (!ptr) {
                    return NULL;
                }
            }

        /* End of set expression */
        } else if (ptr[0] == tok_end || ptr[0] == TOK_AND || !ptr[0]) {
            break;
        }
    } while (true);

    return ptr;
}

static
const char* flecs_parse_arguments(
    const ecs_world_t *world,
    const char *name,
    const char *expr,
    int64_t column,
    const char *ptr,
    char *token,
    ecs_term_t *term)
{
    (void)column;

    int32_t arg = 0;

    do {
        if (flecs_valid_token_start_char(ptr[0])) {
            if (arg == 2) {
                ecs_parser_error(name, expr, (ptr - expr), 
                    "too many arguments in term");
                return NULL;
            }

            ptr = ecs_parse_identifier(name, expr, ptr, token);
            if (!ptr) {
                return NULL;
            }

            ecs_term_id_t *term_id = NULL;

            if (arg == 0) {
                term_id = &term->src;
            } else if (arg == 1) {
                term_id = &term->second;
            }

            /* If token is a colon, the token is an identifier followed by a
             * set expression. */
            if (ptr[0] == TOK_COLON) {
                if (flecs_parse_identifier(token, term_id)) {
                    ecs_parser_error(name, expr, (ptr - expr), 
                        "invalid identifier '%s'", token);
                    return NULL;
                }

                ptr = ecs_parse_ws(ptr + 1);
                ptr = flecs_parse_term_flags(world, name, expr, (ptr - expr), ptr,
                    NULL, term_id, TOK_PAREN_CLOSE);
                if (!ptr) {
                    return NULL;
                }

            /* Check for term flags */
            } else if (!ecs_os_strcmp(token, TOK_CASCADE) ||
                !ecs_os_strcmp(token, TOK_SELF) || 
                !ecs_os_strcmp(token, TOK_UP) || 
                !ecs_os_strcmp(token, TOK_DOWN) || 
                !(ecs_os_strcmp(token, TOK_PARENT)))
            {
                ptr = flecs_parse_term_flags(world, name, expr, (ptr - expr), ptr, 
                    token, term_id, TOK_PAREN_CLOSE);
                if (!ptr) {
                    return NULL;
                }

            /* Regular identifier */
            } else if (flecs_parse_identifier(token, term_id)) {
                ecs_parser_error(name, expr, (ptr - expr), 
                    "invalid identifier '%s'", token);
                return NULL;
            }

            if (ptr[0] == TOK_AND) {
                ptr = ecs_parse_ws(ptr + 1);

                term->id_flags = ECS_PAIR;

            } else if (ptr[0] == TOK_PAREN_CLOSE) {
                ptr = ecs_parse_ws(ptr + 1);
                break;

            } else {
                ecs_parser_error(name, expr, (ptr - expr), 
                    "expected ',' or ')'");
                return NULL;
            }

        } else {
            ecs_parser_error(name, expr, (ptr - expr), 
                "expected identifier or set expression");
            return NULL;
        }

        arg ++;

    } while (true);

    return ptr;
}

static
void flecs_parser_unexpected_char(
    const char *name,
    const char *expr,
    const char *ptr,
    char ch)
{
    if (ch && (ch != '\n')) {
        ecs_parser_error(name, expr, (ptr - expr), 
            "unexpected character '%c'", ch);
    } else {
        ecs_parser_error(name, expr, (ptr - expr), 
            "unexpected end of term");
    }
}

static
const char* flecs_parse_term(
    const ecs_world_t *world,
    const char *name,
    const char *expr,
    ecs_term_t *term_out)
{
    const char *ptr = expr;
    char token[ECS_MAX_TOKEN_SIZE] = {0};
    ecs_term_t term = { .move = true /* parser never owns resources */ };

    ptr = ecs_parse_ws(ptr);

    /* Inout specifiers always come first */
    if (ptr[0] == TOK_BRACKET_OPEN) {
        ptr = flecs_parse_annotation(name, expr, (ptr - expr), ptr + 1, &term.inout);
        if (!ptr) {
            goto error;
        }
        ptr = ecs_parse_ws(ptr);
    }

    if (flecs_valid_operator_char(ptr[0])) {
        term.oper = flecs_parse_operator(ptr[0]);
        ptr = ecs_parse_ws(ptr + 1);
    }

    /* If next token is the start of an identifier, it could be either a type
     * role, source or component identifier */
    if (flecs_valid_identifier_start_char(ptr[0])) {
        ptr = ecs_parse_identifier(name, expr, ptr, token);
        if (!ptr) {
            goto error;
        }

        /* Is token a type role? */
        if (ptr[0] == TOK_BITWISE_OR && ptr[1] != TOK_BITWISE_OR) {
            ptr ++;
            goto flecs_parse_role;
        }

        /* Is token a predicate? */
        if (ptr[0] == TOK_PAREN_OPEN) {
            goto parse_predicate;    
        }

        /* Next token must be a predicate */
        goto parse_predicate;

    /* Pair with implicit subject */
    } else if (ptr[0] == TOK_PAREN_OPEN) {
        goto parse_pair;

    /* Open query scope */
    } else if (ptr[0] == TOK_SCOPE_OPEN) {
        term.first.id = EcsScopeOpen;
        term.src.id = 0;
        term.src.flags = EcsIsEntity;
        term.inout = EcsInOutNone;
        goto parse_done;

    /* Close query scope */
    } else if (ptr[0] == TOK_SCOPE_CLOSE) {
        term.first.id = EcsScopeClose;
        term.src.id = 0;
        term.src.flags = EcsIsEntity;
        term.inout = EcsInOutNone;
        ptr = ecs_parse_ws(ptr + 1);
        goto parse_done;

    /* Nothing else expected here */
    } else {
        flecs_parser_unexpected_char(name, expr, ptr, ptr[0]);
        goto error;
    }

flecs_parse_role:
    term.id_flags = flecs_parse_role(name, expr, (ptr - expr), token);
    if (!term.id_flags) {
        goto error;
    }

    ptr = ecs_parse_ws(ptr);

    /* If next token is the source token, this is an empty source */
    if (flecs_valid_token_start_char(ptr[0])) {
        ptr = ecs_parse_identifier(name, expr, ptr, token);
        if (!ptr) {
            goto error;
        }

        /* If not, it's a predicate */
        goto parse_predicate;

    } else if (ptr[0] == TOK_PAREN_OPEN) {
        goto parse_pair;
    } else {
        ecs_parser_error(name, expr, (ptr - expr), 
            "expected identifier after role");
        goto error;
    }

parse_predicate:
    if (flecs_parse_identifier(token, &term.first)) {
        ecs_parser_error(name, expr, (ptr - expr), 
            "invalid identifier '%s'", token); 
        goto error;
    }

    /* Set expression */
    if (ptr[0] == TOK_COLON) {
        ptr = ecs_parse_ws(ptr + 1);
        ptr = flecs_parse_term_flags(world, name, expr, (ptr - expr), ptr, NULL, 
            &term.first, TOK_COLON);
        if (!ptr) {
            goto error;
        }

        ptr = ecs_parse_ws(ptr);

        if (ptr[0] == TOK_AND || !ptr[0]) {
            goto parse_done;
        }

        if (ptr[0] != TOK_COLON) {
            ecs_parser_error(name, expr, (ptr - expr), 
                "unexpected token '%c' after predicate set expression", ptr[0]);
            goto error;
        }

        ptr = ecs_parse_ws(ptr + 1);
    } else if (!ecs_os_strncmp(ptr, TOK_EQ, 2)) {
        ptr = ecs_parse_ws(ptr + 2);
        goto parse_eq;
    } else if (!ecs_os_strncmp(ptr, TOK_NEQ, 2)) {
        ptr = ecs_parse_ws(ptr + 2);
        goto parse_neq;
    } else if (!ecs_os_strncmp(ptr, TOK_MATCH, 2)) {
        ptr = ecs_parse_ws(ptr + 2);
        goto parse_match;
    } else {
        ptr = ecs_parse_ws(ptr);
    }

    if (ptr[0] == TOK_PAREN_OPEN) {
        ptr ++;
        if (ptr[0] == TOK_PAREN_CLOSE) {
            term.src.flags = EcsIsEntity;
            term.src.id = 0;
            ptr ++;
            ptr = ecs_parse_ws(ptr);
        } else {
            ptr = flecs_parse_arguments(
                world, name, expr, (ptr - expr), ptr, token, &term);
        }

        goto parse_done;
    }

    goto parse_done;

parse_eq:
    term.src = term.first;
    term.first = (ecs_term_id_t){0};
    term.first.id = EcsPredEq;
    goto parse_right_operand;

parse_neq:
    term.src = term.first;
    term.first = (ecs_term_id_t){0};
    term.first.id = EcsPredEq;
    if (term.oper != EcsAnd) {
        ecs_parser_error(name, expr, (ptr - expr), 
            "invalid operator combination");
        goto error;
    }
    term.oper = EcsNot;
    goto parse_right_operand;
    
parse_match:
    term.src = term.first;
    term.first = (ecs_term_id_t){0};
    term.first.id = EcsPredMatch;
    goto parse_right_operand;

parse_right_operand:
    if (flecs_valid_token_start_char(ptr[0])) {
        ptr = ecs_parse_identifier(name, expr, ptr, token);
        if (!ptr) {
            goto error;
        }

        if (term.first.id == EcsPredMatch) {
            if (token[0] == '"' && token[1] == '!') {
                term.oper = EcsNot;
            }
        }

        if (flecs_parse_identifier(token, &term.second)) {
            ecs_parser_error(name, expr, (ptr - expr), 
                "invalid identifier '%s'", token); 
            goto error;
        }

        term.src.flags &= ~EcsTraverseFlags;
        term.src.flags |= EcsSelf;
        term.inout = EcsInOutNone;
    } else {
        ecs_parser_error(name, expr, (ptr - expr), 
            "expected identifier");
        goto error;
    }
    goto parse_done;
parse_pair:
    ptr = ecs_parse_identifier(name, expr, ptr + 1, token);
    if (!ptr) {
        goto error;
    }

    if (ptr[0] == TOK_COLON) {
        ptr = ecs_parse_ws(ptr + 1);
        ptr = flecs_parse_term_flags(world, name, expr, (ptr - expr), ptr,
            NULL, &term.first, TOK_PAREN_CLOSE);
        if (!ptr) {
            goto error;
        }
    }

    if (ptr[0] == TOK_AND) {
        ptr = ecs_parse_ws(ptr + 1);
        if (ptr[0] == TOK_PAREN_CLOSE) {
            ecs_parser_error(name, expr, (ptr - expr), 
                "expected identifier for second element of pair"); 
            goto error;
        }
        
        term.src.id = EcsThis;
        term.src.flags |= EcsIsVariable;
        goto parse_pair_predicate;
    } else if (ptr[0] == TOK_PAREN_CLOSE) {
        term.src.id = EcsThis;
        term.src.flags |= EcsIsVariable;
        goto parse_pair_predicate;
    } else {
        flecs_parser_unexpected_char(name, expr, ptr, ptr[0]);
        goto error;
    }

parse_pair_predicate:
    if (flecs_parse_identifier(token, &term.first)) {
        ecs_parser_error(name, expr, (ptr - expr), 
            "invalid identifier '%s'", token); 
        goto error;
    }

    ptr = ecs_parse_ws(ptr);
    if (flecs_valid_token_start_char(ptr[0])) {
        ptr = ecs_parse_identifier(name, expr, ptr, token);
        if (!ptr) {
            goto error;
        }

        if (ptr[0] == TOK_COLON) {
            ptr = ecs_parse_ws(ptr + 1);
            ptr = flecs_parse_term_flags(world, name, expr, (ptr - expr), ptr,
                NULL, &term.second, TOK_PAREN_CLOSE);
            if (!ptr) {
                goto error;
            }
        }

        if (ptr[0] == TOK_PAREN_CLOSE) {
            ptr ++;
            goto parse_pair_object;
        } else {
            flecs_parser_unexpected_char(name, expr, ptr, ptr[0]);
            goto error;
        }
    } else if (ptr[0] == TOK_PAREN_CLOSE) {
        /* No object */
        ptr ++;
        goto parse_done;
    } else {
        ecs_parser_error(name, expr, (ptr - expr), 
            "expected pair object or ')'");
        goto error;
    }

parse_pair_object:
    if (flecs_parse_identifier(token, &term.second)) {
        ecs_parser_error(name, expr, (ptr - expr), 
            "invalid identifier '%s'", token); 
        goto error;
    }

    if (term.id_flags != 0) {
        if (!ECS_HAS_ID_FLAG(term.id_flags, PAIR)) {
            ecs_parser_error(name, expr, (ptr - expr), 
                "invalid combination of role '%s' with pair", 
                    ecs_id_flag_str(term.id_flags));
            goto error;
        }
    } else {
        term.id_flags = ECS_PAIR;
    }

    ptr = ecs_parse_ws(ptr);
    goto parse_done;

parse_done:
    *term_out = term;
    return ptr;

error:
    ecs_term_fini(&term);
    *term_out = (ecs_term_t){0};
    return NULL;
}

static
bool flecs_is_valid_end_of_term(
    const char *ptr)
{
    if ((ptr[0] == TOK_AND) ||    /* another term with And operator */
        (ptr[0] == TOK_OR[0]) ||  /* another term with Or operator */
        (ptr[0] == '\n') ||       /* newlines are valid */
        (ptr[0] == '\0') ||       /* end of string */
        (ptr[0] == '/') ||        /* comment (in plecs) */
        (ptr[0] == '{') ||        /* scope (in plecs) */
        (ptr[0] == '}') ||
        (ptr[0] == ':') ||        /* inheritance (in plecs) */
        (ptr[0] == '='))          /* assignment (in plecs) */
    {
        return true;
    }
    return false;
}

char* ecs_parse_term(
    const ecs_world_t *world,
    const char *name,
    const char *expr,
    const char *ptr,
    ecs_term_t *term)
{
    ecs_check(world != NULL, ECS_INVALID_PARAMETER, NULL);
    ecs_check(ptr != NULL, ECS_INVALID_PARAMETER, NULL);
    ecs_check(term != NULL, ECS_INVALID_PARAMETER, NULL);

    ecs_term_id_t *src = &term->src;

    if (ptr != expr) {
        if (ptr[0]) {
            if (ptr[0] == ',') {
                ptr ++;
            } else if (ptr[0] == '|') {
                ptr += 2;
            } else if (ptr[0] == '{') {
                ptr ++;
            } else if (ptr[0] == '}') {
                /* nothing to be done */
            } else {
                ecs_parser_error(name, expr, (ptr - expr), 
                    "invalid preceding token");
            }
        }
    }
    
    ptr = ecs_parse_ws_eol(ptr);
    if (!ptr[0]) {
        *term = (ecs_term_t){0};
        return (char*)ptr;
    }

    if (ptr == expr && !strcmp(expr, "0")) {
        return (char*)&ptr[1];
    }

    /* Parse next element */
    ptr = flecs_parse_term(world, name, ptr, term);
    if (!ptr) {
        goto error;
    }

    /* Check for $() notation */
    if (term->first.name && !ecs_os_strcmp(term->first.name, "$")) {
        if (term->src.name) {
            ecs_os_free(term->first.name);
            
            term->first = term->src;

            if (term->second.name) {
                term->src = term->second;       
            } else {
                term->src.id = EcsThis;
                term->src.name = NULL;
                term->src.flags |= EcsIsVariable;
            }

            term->second.name = ecs_os_strdup(term->first.name);
            term->second.flags |= EcsIsVariable;
        }
    }

    /* Post-parse consistency checks */

    /* If next token is OR, term is part of an OR expression */
    if (!ecs_os_strncmp(ptr, TOK_OR, 2)) {
        /* An OR operator must always follow an AND or another OR */
        if (term->oper != EcsAnd) {
            ecs_parser_error(name, expr, (ptr - expr), 
                "cannot combine || with other operators");
            goto error;
        }

        term->oper = EcsOr;
    }

    /* Term must either end in end of expression, AND or OR token */
    if (!flecs_is_valid_end_of_term(ptr)) {
        if (!flecs_isident(ptr[0]) || ((ptr != expr) && (ptr[-1] != ' '))) {
            ecs_parser_error(name, expr, (ptr - expr), 
                "expected end of expression or next term");
            goto error;
        }
    }

    /* If the term just contained a 0, the expression has nothing. Ensure
     * that after the 0 nothing else follows */
    if (term->first.name && !ecs_os_strcmp(term->first.name, "0")) {
        if (ptr[0]) {
            ecs_parser_error(name, expr, (ptr - expr), 
                "unexpected term after 0"); 
            goto error;
        }

        if (src->flags != 0) {
            ecs_parser_error(name, expr, (ptr - expr), 
                "invalid combination of 0 with non-default subject");
            goto error;
        }

        src->flags = EcsIsEntity;
        src->id = 0;
        ecs_os_free(term->first.name);
        term->first.name = NULL;
    }

    /* Cannot combine EcsIsEntity/0 with operators other than AND */
    if (term->oper != EcsAnd && ecs_term_match_0(term)) {
        if (term->first.id != EcsScopeOpen && term->first.id != EcsScopeClose) {
            ecs_parser_error(name, expr, (ptr - expr), 
                "invalid operator for empty source"); 
            goto error;
        }
    }

    /* Automatically assign This if entity is not assigned and the set is
     * nothing */
    if (!(src->flags & EcsIsEntity)) {
        if (!src->name) {
            if (!src->id) {
                src->id = EcsThis;
                src->flags |= EcsIsVariable;
            }
        }
    }

    if (src->name && !ecs_os_strcmp(src->name, "0")) {
        src->id = 0;
        src->flags = EcsIsEntity;
    }

    /* Process role */
    if (term->id_flags == ECS_AND) {
        term->oper = EcsAndFrom;
        term->id_flags = 0;
    } else if (term->id_flags == ECS_OR) {
        term->oper = EcsOrFrom;
        term->id_flags = 0;
    } else if (term->id_flags == ECS_NOT) {
        term->oper = EcsNotFrom;
        term->id_flags = 0;
    }

    ptr = ecs_parse_ws(ptr);

    return (char*)ptr;
error:
    if (term) {
        ecs_term_fini(term);
    }
    return NULL;
}

#endif

/**
 * @file addons/meta.c
 * @brief C utilities for meta addon.
 */


#ifdef FLECS_META_C

#include <ctype.h>

#define ECS_META_IDENTIFIER_LENGTH (256)

#define ecs_meta_error(ctx, ptr, ...)\
    ecs_parser_error((ctx)->name, (ctx)->desc, ptr - (ctx)->desc, __VA_ARGS__);

typedef char ecs_meta_token_t[ECS_META_IDENTIFIER_LENGTH];

typedef struct meta_parse_ctx_t {
    const char *name;
    const char *desc;
} meta_parse_ctx_t;

typedef struct meta_type_t {
    ecs_meta_token_t type;
    ecs_meta_token_t params;
    bool is_const;
    bool is_ptr;
} meta_type_t;

typedef struct meta_member_t {
    meta_type_t type;
    ecs_meta_token_t name;
    int64_t count;
    bool is_partial;
} meta_member_t;

typedef struct meta_constant_t {
    ecs_meta_token_t name;
    int64_t value;
    bool is_value_set;
} meta_constant_t;

typedef struct meta_params_t {
    meta_type_t key_type;
    meta_type_t type;
    int64_t count;
    bool is_key_value;
    bool is_fixed_size;
} meta_params_t;

static
const char* skip_scope(const char *ptr, meta_parse_ctx_t *ctx) {
    /* Keep track of which characters were used to open the scope */
    char stack[256];
    int32_t sp = 0;
    char ch;

    while ((ch = *ptr)) {
        if (ch == '(' || ch == '<') {
            stack[sp] = ch;

            sp ++;
            if (sp >= 256) {
                ecs_meta_error(ctx, ptr, "maximum level of nesting reached");
                goto error;
            }            
        } else if (ch == ')' || ch == '>') {
            sp --;
            if ((sp < 0) || (ch == '>' && stack[sp] != '<') || 
                (ch == ')' && stack[sp] != '(')) 
            {
                ecs_meta_error(ctx, ptr, "mismatching %c in identifier", ch);
                goto error;
            }
        }

        ptr ++;

        if (!sp) {
            break;
        }
    }

    return ptr;
error:
    return NULL;
}

static
const char* parse_c_digit(
    const char *ptr,
    int64_t *value_out)
{
    char token[24];
    ptr = ecs_parse_ws_eol(ptr);
    ptr = ecs_parse_digit(ptr, token);
    if (!ptr) {
        goto error;
    }

    *value_out = strtol(token, NULL, 0);

    return ecs_parse_ws_eol(ptr);
error:
    return NULL;
}

static
const char* parse_c_identifier(
    const char *ptr, 
    char *buff,
    char *params,
    meta_parse_ctx_t *ctx) 
{
    ecs_assert(ptr != NULL, ECS_INTERNAL_ERROR, NULL);
    ecs_assert(buff != NULL, ECS_INTERNAL_ERROR, NULL);
    ecs_assert(ctx != NULL, ECS_INTERNAL_ERROR, NULL);

    char *bptr = buff, ch;

    if (params) {
        params[0] = '\0';
    }

    /* Ignore whitespaces */
    ptr = ecs_parse_ws_eol(ptr);
    ch = *ptr;

    if (!isalpha(ch) && (ch != '_')) {
        ecs_meta_error(ctx, ptr, "invalid identifier (starts with '%c')", ch);
        goto error;
    }

    while ((ch = *ptr) && !isspace(ch) && ch != ';' && ch != ',' && ch != ')' && 
        ch != '>' && ch != '}' && ch != '*') 
    {
        /* Type definitions can contain macros or templates */
        if (ch == '(' || ch == '<') {
            if (!params) {
                ecs_meta_error(ctx, ptr, "unexpected %c", *ptr);
                goto error;
            }

            const char *end = skip_scope(ptr, ctx);
            ecs_os_strncpy(params, ptr, (ecs_size_t)(end - ptr));
            params[end - ptr] = '\0';

            ptr = end;
        } else {
            *bptr = ch;
            bptr ++;
            ptr ++;
        }
    }

    *bptr = '\0';

    if (!ch) {
        ecs_meta_error(ctx, ptr, "unexpected end of token");
        goto error;
    }

    return ptr;
error:
    return NULL;
}

static
const char * meta_open_scope(
    const char *ptr,
    meta_parse_ctx_t *ctx)    
{
    /* Skip initial whitespaces */
    ptr = ecs_parse_ws_eol(ptr);

    /* Is this the start of the type definition? */
    if (ctx->desc == ptr) {
        if (*ptr != '{') {
            ecs_meta_error(ctx, ptr, "missing '{' in struct definition");
            goto error; 
        }

        ptr ++;
        ptr = ecs_parse_ws_eol(ptr);
    }

    /* Is this the end of the type definition? */
    if (!*ptr) {
        ecs_meta_error(ctx, ptr, "missing '}' at end of struct definition");
        goto error;
    }   

    /* Is this the end of the type definition? */
    if (*ptr == '}') {
        ptr = ecs_parse_ws_eol(ptr + 1);
        if (*ptr) {
            ecs_meta_error(ctx, ptr, 
                "stray characters after struct definition");
            goto error;
        }
        return NULL;
    }

    return ptr;
error:
    return NULL;
}

static
const char* meta_parse_constant(
    const char *ptr,
    meta_constant_t *token,
    meta_parse_ctx_t *ctx)
{    
    ptr = meta_open_scope(ptr, ctx);
    if (!ptr) {
        return NULL;
    }

    token->is_value_set = false;

    /* Parse token, constant identifier */
    ptr = parse_c_identifier(ptr, token->name, NULL, ctx);
    if (!ptr) {
        return NULL;
    }

    ptr = ecs_parse_ws_eol(ptr);
    if (!ptr) {
        return NULL;
    }

    /* Explicit value assignment */
    if (*ptr == '=') {
        int64_t value = 0;
        ptr = parse_c_digit(ptr + 1, &value);
        token->value = value;
        token->is_value_set = true;
    }

    /* Expect a ',' or '}' */
    if (*ptr != ',' && *ptr != '}') {
        ecs_meta_error(ctx, ptr, "missing , after enum constant");
        goto error;
    }

    if (*ptr == ',') {
        return ptr + 1;
    } else {
        return ptr;
    }
error:
    return NULL;
}

static
const char* meta_parse_type(
    const char *ptr,
    meta_type_t *token,
    meta_parse_ctx_t *ctx)
{
    token->is_ptr = false;
    token->is_const = false;

    ptr = ecs_parse_ws_eol(ptr);

    /* Parse token, expect type identifier or ECS_PROPERTY */
    ptr = parse_c_identifier(ptr, token->type, token->params, ctx);
    if (!ptr) {
        goto error;
    }

    if (!strcmp(token->type, "ECS_PRIVATE")) {
        /* Members from this point are not stored in metadata */
        ptr += ecs_os_strlen(ptr);
        goto done;
    }

    /* If token is const, set const flag and continue parsing type */
    if (!strcmp(token->type, "const")) {
        token->is_const = true;

        /* Parse type after const */
        ptr = parse_c_identifier(ptr + 1, token->type, token->params, ctx);
    }

    /* Check if type is a pointer */
    ptr = ecs_parse_ws_eol(ptr);
    if (*ptr == '*') {
        token->is_ptr = true;
        ptr ++;
    }

done:
    return ptr;
error:
    return NULL;
}

static
const char* meta_parse_member(
    const char *ptr,
    meta_member_t *token,
    meta_parse_ctx_t *ctx)
{
    ptr = meta_open_scope(ptr, ctx);
    if (!ptr) {
        return NULL;
    }

    token->count = 1;
    token->is_partial = false;

    /* Parse member type */
    ptr = meta_parse_type(ptr, &token->type, ctx);
    if (!ptr) {
        token->is_partial = true;
        goto error;
    }

    if (!ptr[0]) {
        return ptr;        
    }

    /* Next token is the identifier */
    ptr = parse_c_identifier(ptr, token->name, NULL, ctx);
    if (!ptr) {
        goto error;
    }

    /* Skip whitespace between member and [ or ; */
    ptr = ecs_parse_ws_eol(ptr);

    /* Check if this is an array */
    char *array_start = strchr(token->name, '[');
    if (!array_start) {
        /* If the [ was separated by a space, it will not be parsed as part of
         * the name */
        if (*ptr == '[') {
            array_start = (char*)ptr; /* safe, will not be modified */
        }
    }

    if (array_start) {
        /* Check if the [ matches with a ] */
        char *array_end = strchr(array_start, ']');
        if (!array_end) {
            ecs_meta_error(ctx, ptr, "missing ']'");
            goto error;

        } else if (array_end - array_start == 0) {
            ecs_meta_error(ctx, ptr, "dynamic size arrays are not supported");
            goto error;
        }

        token->count = atoi(array_start + 1);

        if (array_start == ptr) {
            /* If [ was found after name, continue parsing after ] */
            ptr = array_end + 1;
        } else {
            /* If [ was fonud in name, replace it with 0 terminator */
            array_start[0] = '\0';
        }
    }

    /* Expect a ; */
    if (*ptr != ';') {
        ecs_meta_error(ctx, ptr, "missing ; after member declaration");
        goto error;
    }

    return ptr + 1;
error:
    return NULL;
}

static
int meta_parse_desc(
    const char *ptr,
    meta_params_t *token,
    meta_parse_ctx_t *ctx)
{
    token->is_key_value = false;
    token->is_fixed_size = false;

    ptr = ecs_parse_ws_eol(ptr);
    if (*ptr != '(' && *ptr != '<') {
        ecs_meta_error(ctx, ptr, 
            "expected '(' at start of collection definition");
        goto error;
    }

    ptr ++;

    /* Parse type identifier */
    ptr = meta_parse_type(ptr, &token->type, ctx);
    if (!ptr) {
        goto error;
    }

    ptr = ecs_parse_ws_eol(ptr);

    /* If next token is a ',' the first type was a key type */
    if (*ptr == ',') {
        ptr = ecs_parse_ws_eol(ptr + 1);
        
        if (isdigit(*ptr)) {
            int64_t value;
            ptr = parse_c_digit(ptr, &value);
            if (!ptr) {
                goto error;
            }

            token->count = value;
            token->is_fixed_size = true;
        } else {
            token->key_type = token->type;

            /* Parse element type */
            ptr = meta_parse_type(ptr, &token->type, ctx);
            ptr = ecs_parse_ws_eol(ptr);

            token->is_key_value = true;
        }
    }

    if (*ptr != ')' && *ptr != '>') {
        ecs_meta_error(ctx, ptr, 
            "expected ')' at end of collection definition");
        goto error;
    }

    return 0;
error:
    return -1;
}

static
ecs_entity_t meta_lookup(
    ecs_world_t *world,
    meta_type_t *token,
    const char *ptr,
    int64_t count,
    meta_parse_ctx_t *ctx);

static
ecs_entity_t meta_lookup_array(
    ecs_world_t *world,
    ecs_entity_t e,
    const char *params_decl,
    meta_parse_ctx_t *ctx)
{
    meta_parse_ctx_t param_ctx = {
        .name = ctx->name,
        .desc = params_decl
    };

    meta_params_t params;
    if (meta_parse_desc(params_decl, &params, &param_ctx)) {
        goto error;
    }
    if (!params.is_fixed_size) {
        ecs_meta_error(ctx, params_decl, "missing size for array");
        goto error;
    }

    if (!params.count) {
        ecs_meta_error(ctx, params_decl, "invalid array size");
        goto error;
    }

    ecs_entity_t element_type = ecs_lookup_symbol(world, params.type.type, true);
    if (!element_type) {
        ecs_meta_error(ctx, params_decl, "unknown element type '%s'",
            params.type.type);
    }

    if (!e) {
        e = ecs_new_id(world);
    }

    ecs_check(params.count <= INT32_MAX, ECS_INVALID_PARAMETER, NULL);

    return ecs_set(world, e, EcsArray, { element_type, (int32_t)params.count });
error:
    return 0;
}

static
ecs_entity_t meta_lookup_vector(
    ecs_world_t *world,
    ecs_entity_t e,
    const char *params_decl,
    meta_parse_ctx_t *ctx)
{
    meta_parse_ctx_t param_ctx = {
        .name = ctx->name,
        .desc = params_decl
    };

    meta_params_t params;
    if (meta_parse_desc(params_decl, &params, &param_ctx)) {
        goto error;
    }

    if (params.is_key_value) {
        ecs_meta_error(ctx, params_decl,
            "unexpected key value parameters for vector");
        goto error;
    }

    ecs_entity_t element_type = meta_lookup(
        world, &params.type, params_decl, 1, &param_ctx);

    if (!e) {
        e = ecs_new_id(world);
    }

    return ecs_set(world, e, EcsVector, { element_type });
error:
    return 0;
}

static
ecs_entity_t meta_lookup_bitmask(
    ecs_world_t *world,
    ecs_entity_t e,
    const char *params_decl,
    meta_parse_ctx_t *ctx)
{
    (void)e;

    meta_parse_ctx_t param_ctx = {
        .name = ctx->name,
        .desc = params_decl
    };

    meta_params_t params;
    if (meta_parse_desc(params_decl, &params, &param_ctx)) {
        goto error;
    }

    if (params.is_key_value) {
        ecs_meta_error(ctx, params_decl,
            "unexpected key value parameters for bitmask");
        goto error;
    }

    if (params.is_fixed_size) {
        ecs_meta_error(ctx, params_decl,
            "unexpected size for bitmask");
        goto error;
    }

    ecs_entity_t bitmask_type = meta_lookup(
        world, &params.type, params_decl, 1, &param_ctx);
    ecs_check(bitmask_type != 0, ECS_INVALID_PARAMETER, NULL);

#ifndef FLECS_NDEBUG
    /* Make sure this is a bitmask type */
    const EcsMetaType *type_ptr = ecs_get(world, bitmask_type, EcsMetaType);
    ecs_check(type_ptr != NULL, ECS_INVALID_PARAMETER, NULL);
    ecs_check(type_ptr->kind == EcsBitmaskType, ECS_INVALID_PARAMETER, NULL);
#endif

    return bitmask_type;
error:
    return 0;
}

static
ecs_entity_t meta_lookup(
    ecs_world_t *world,
    meta_type_t *token,
    const char *ptr,
    int64_t count,
    meta_parse_ctx_t *ctx)
{
    ecs_assert(world != NULL, ECS_INTERNAL_ERROR, NULL);
    ecs_assert(token != NULL, ECS_INTERNAL_ERROR, NULL);
    ecs_assert(ptr != NULL, ECS_INTERNAL_ERROR, NULL);
    ecs_assert(ctx != NULL, ECS_INTERNAL_ERROR, NULL);

    const char *typename = token->type;
    ecs_entity_t type = 0;

    /* Parse vector type */
    if (!token->is_ptr) {
        if (!ecs_os_strcmp(typename, "ecs_array")) {
            type = meta_lookup_array(world, 0, token->params, ctx);

        } else if (!ecs_os_strcmp(typename, "ecs_vector") || 
                !ecs_os_strcmp(typename, "flecs::vector")) 
        {
            type = meta_lookup_vector(world, 0, token->params, ctx);

        } else if (!ecs_os_strcmp(typename, "flecs::bitmask")) {
            type = meta_lookup_bitmask(world, 0, token->params, ctx);

        } else if (!ecs_os_strcmp(typename, "flecs::byte")) {
            type = ecs_id(ecs_byte_t);

        } else if (!ecs_os_strcmp(typename, "char")) {
            type = ecs_id(ecs_char_t);

        } else if (!ecs_os_strcmp(typename, "bool") || 
                !ecs_os_strcmp(typename, "_Bool")) 
        {
            type = ecs_id(ecs_bool_t);

        } else if (!ecs_os_strcmp(typename, "int8_t")) {
            type = ecs_id(ecs_i8_t);
        } else if (!ecs_os_strcmp(typename, "int16_t")) {
            type = ecs_id(ecs_i16_t);
        } else if (!ecs_os_strcmp(typename, "int32_t")) {
            type = ecs_id(ecs_i32_t);
        } else if (!ecs_os_strcmp(typename, "int64_t")) {
            type = ecs_id(ecs_i64_t);

        } else if (!ecs_os_strcmp(typename, "uint8_t")) {
            type = ecs_id(ecs_u8_t);
        } else if (!ecs_os_strcmp(typename, "uint16_t")) {
            type = ecs_id(ecs_u16_t);
        } else if (!ecs_os_strcmp(typename, "uint32_t")) {
            type = ecs_id(ecs_u32_t);
        } else if (!ecs_os_strcmp(typename, "uint64_t")) {
            type = ecs_id(ecs_u64_t);

        } else if (!ecs_os_strcmp(typename, "float")) {
            type = ecs_id(ecs_f32_t);
        } else if (!ecs_os_strcmp(typename, "double")) {
            type = ecs_id(ecs_f64_t);

        } else if (!ecs_os_strcmp(typename, "ecs_entity_t")) {
            type = ecs_id(ecs_entity_t);

        } else if (!ecs_os_strcmp(typename, "char*")) {
            type = ecs_id(ecs_string_t);
        } else {
            type = ecs_lookup_symbol(world, typename, true);
        }
    } else {
        if (!ecs_os_strcmp(typename, "char")) {
            typename = "flecs.meta.string";
        } else
        if (token->is_ptr) {
            typename = "flecs.meta.uptr";
        } else
        if (!ecs_os_strcmp(typename, "char*") || 
            !ecs_os_strcmp(typename, "flecs::string")) 
        {
            typename = "flecs.meta.string";
        }

        type = ecs_lookup_symbol(world, typename, true);
    }

    if (count != 1) {
        ecs_check(count <= INT32_MAX, ECS_INVALID_PARAMETER, NULL);

        type = ecs_set(world, 0, EcsArray, {type, (int32_t)count});
    }

    if (!type) {
        ecs_meta_error(ctx, ptr, "unknown type '%s'", typename);
        goto error;
    }

    return type;
error:
    return 0;
}

static
int meta_parse_struct(
    ecs_world_t *world,
    ecs_entity_t t,
    const char *desc)
{
    const char *ptr = desc;
    const char *name = ecs_get_name(world, t);

    meta_member_t token;
    meta_parse_ctx_t ctx = {
        .name = name,
        .desc = ptr
    };

    ecs_entity_t old_scope = ecs_set_scope(world, t);

    while ((ptr = meta_parse_member(ptr, &token, &ctx)) && ptr[0]) {
        ecs_entity_t m = ecs_entity(world, {
            .name = token.name
        });

        ecs_entity_t type = meta_lookup(
            world, &token.type, ptr, 1, &ctx);
        if (!type) {
            goto error;
        }

        ecs_set(world, m, EcsMember, {
            .type = type, 
            .count = (ecs_size_t)token.count
        });
    }

    ecs_set_scope(world, old_scope);

    return 0;
error:
    return -1;
}

static
int meta_parse_constants(
    ecs_world_t *world,
    ecs_entity_t t,
    const char *desc,
    bool is_bitmask)
{
    ecs_assert(world != NULL, ECS_INTERNAL_ERROR, NULL);
    ecs_assert(t != 0, ECS_INTERNAL_ERROR, NULL);
    ecs_assert(desc != NULL, ECS_INTERNAL_ERROR, NULL);

    const char *ptr = desc;
    const char *name = ecs_get_name(world, t);

    meta_parse_ctx_t ctx = {
        .name = name,
        .desc = ptr
    };

    meta_constant_t token;
    int64_t last_value = 0;

    ecs_entity_t old_scope = ecs_set_scope(world, t);

    while ((ptr = meta_parse_constant(ptr, &token, &ctx))) {
        if (token.is_value_set) {
            last_value = token.value;
        } else if (is_bitmask) {
            ecs_meta_error(&ctx, ptr,
                "bitmask requires explicit value assignment");
            goto error;
        }

        ecs_entity_t c = ecs_entity(world, {
            .name = token.name
        });

        if (!is_bitmask) {
            ecs_set_pair_object(world, c, EcsConstant, ecs_i32_t, 
                {(ecs_i32_t)last_value});
        } else {
            ecs_set_pair_object(world, c, EcsConstant, ecs_u32_t, 
                {(ecs_u32_t)last_value});
        }

        last_value ++;
    }

    ecs_set_scope(world, old_scope);

    return 0;
error:
    return -1;
}

static
int meta_parse_enum(
    ecs_world_t *world,
    ecs_entity_t t,
    const char *desc)
{
    ecs_add(world, t, EcsEnum);
    return meta_parse_constants(world, t, desc, false);
}

static
int meta_parse_bitmask(
    ecs_world_t *world,
    ecs_entity_t t,
    const char *desc)
{
    ecs_add(world, t, EcsBitmask);
    return meta_parse_constants(world, t, desc, true);
}

int ecs_meta_from_desc(
    ecs_world_t *world,
    ecs_entity_t component,
    ecs_type_kind_t kind,
    const char *desc)
{
    switch(kind) {
    case EcsStructType:
        if (meta_parse_struct(world, component, desc)) {
            goto error;
        }
        break;
    case EcsEnumType:
        if (meta_parse_enum(world, component, desc)) {
            goto error;
        }
        break;
    case EcsBitmaskType:
        if (meta_parse_bitmask(world, component, desc)) {
            goto error;
        }
        break;
    default:
        break;
    }

    return 0;
error:
    return -1;
}

#endif

/**
 * @file addons/app.c
 * @brief App addon.
 */


#ifdef FLECS_APP

static
int flecs_default_run_action(
    ecs_world_t *world,
    ecs_app_desc_t *desc)
{
    if (desc->init) {
        desc->init(world);
    }

    int result = 0;
    if (desc->frames) {
        int32_t i;
        for (i = 0; i < desc->frames; i ++) {
            if ((result = ecs_app_run_frame(world, desc)) != 0) {
                break;
            }
        }
    } else {
        while ((result = ecs_app_run_frame(world, desc)) == 0) { }
    }

    /* Ensure quit flag is set on world, which can be used to determine if
     * world needs to be cleaned up. */
    ecs_quit(world);

    if (result == 1) {
        return 0; /* Normal exit */
    } else {
        return result; /* Error code */
    }
}

static
int flecs_default_frame_action(
    ecs_world_t *world,
    const ecs_app_desc_t *desc)
{
    return !ecs_progress(world, desc->delta_time);
}

static ecs_app_run_action_t run_action = flecs_default_run_action;
static ecs_app_frame_action_t frame_action = flecs_default_frame_action;
static ecs_app_desc_t ecs_app_desc;

/* Serve REST API from wasm image when running in emscripten */
#ifdef ECS_TARGET_EM
#include <emscripten.h>

ecs_http_server_t *flecs_wasm_rest_server;

EMSCRIPTEN_KEEPALIVE
char* flecs_explorer_request(const char *method, char *request) {
    ecs_http_reply_t reply = ECS_HTTP_REPLY_INIT;
    ecs_http_server_request(flecs_wasm_rest_server, method, request, &reply);
    if (reply.code == 200) {
        return ecs_strbuf_get(&reply.body);
    } else {
        char *body = ecs_strbuf_get(&reply.body);
        if (body) {
            return body;
        } else {
            return ecs_asprintf(
                "{\"error\": \"bad request (code %d)\"}", reply.code);
        }
    }
}
#endif

int ecs_app_run(
    ecs_world_t *world,
    ecs_app_desc_t *desc)
{
    ecs_app_desc = *desc;

    /* Don't set FPS & threads if custom run action is set, as the platform on
     * which the app is running may not support it. */
    if (run_action == flecs_default_run_action) {
        if (ecs_app_desc.target_fps != 0) {
            ecs_set_target_fps(world, ecs_app_desc.target_fps);
        }
        if (ecs_app_desc.threads) {
            ecs_set_threads(world, ecs_app_desc.threads);
        }
    }

    /* REST server enables connecting to app with explorer */
    if (desc->enable_rest) {
#ifdef FLECS_REST
#ifdef ECS_TARGET_EM
        flecs_wasm_rest_server = ecs_rest_server_init(world, NULL);
#else
        ecs_set(world, EcsWorld, EcsRest, {.port = desc->port });
#endif
#else
        ecs_warn("cannot enable remote API, REST addon not available");
#endif
    }

    /* Monitoring periodically collects statistics */
    if (desc->enable_monitor) {
#ifdef FLECS_MONITOR
        ECS_IMPORT(world, FlecsMonitor);
#else
        ecs_warn("cannot enable monitoring, MONITOR addon not available");
#endif
    }

    return run_action(world, &ecs_app_desc);
}

int ecs_app_run_frame(
    ecs_world_t *world,
    const ecs_app_desc_t *desc)
{
    return frame_action(world, desc);
}

int ecs_app_set_run_action(
    ecs_app_run_action_t callback)
{
    if (run_action != flecs_default_run_action && run_action != callback) {
        ecs_err("run action already set");
        return -1;
    }

    run_action = callback;

    return 0;
}

int ecs_app_set_frame_action(
    ecs_app_frame_action_t callback)
{
    if (frame_action != flecs_default_frame_action && frame_action != callback) {
        ecs_err("frame action already set");
        return -1;
    }

    frame_action = callback;

    return 0;
}

#endif

/**
 * @file world.c
 * @brief World-level API.
 */


/* Id flags */
const ecs_id_t ECS_PAIR =                                          (1ull << 63);
const ecs_id_t ECS_OVERRIDE =                                      (1ull << 62);
const ecs_id_t ECS_TOGGLE =                                        (1ull << 61);
const ecs_id_t ECS_AND =                                           (1ull << 60);

/** Builtin component ids */
const ecs_entity_t ecs_id(EcsComponent) =                                   1;
const ecs_entity_t ecs_id(EcsIdentifier) =                                  2;
const ecs_entity_t ecs_id(EcsIterable) =                                    3;
const ecs_entity_t ecs_id(EcsPoly) =                                        4;

/* Poly target components */
const ecs_entity_t EcsQuery =                                               5;
const ecs_entity_t EcsObserver =                                            6;
const ecs_entity_t EcsSystem =                                              7;

/* Core scopes & entities */
const ecs_entity_t EcsWorld =                       FLECS_HI_COMPONENT_ID + 0;
const ecs_entity_t EcsFlecs =                       FLECS_HI_COMPONENT_ID + 1;
const ecs_entity_t EcsFlecsCore =                   FLECS_HI_COMPONENT_ID + 2;
const ecs_entity_t EcsFlecsInternals =              FLECS_HI_COMPONENT_ID + 3;
const ecs_entity_t EcsModule =                      FLECS_HI_COMPONENT_ID + 4;
const ecs_entity_t EcsPrivate =                     FLECS_HI_COMPONENT_ID + 5;
const ecs_entity_t EcsPrefab =                      FLECS_HI_COMPONENT_ID + 6;
const ecs_entity_t EcsDisabled =                    FLECS_HI_COMPONENT_ID + 7;

const ecs_entity_t EcsSlotOf =                      FLECS_HI_COMPONENT_ID + 8;
const ecs_entity_t EcsFlag =                        FLECS_HI_COMPONENT_ID + 9;

/* Relationship properties */
const ecs_entity_t EcsWildcard =                    FLECS_HI_COMPONENT_ID + 10;
const ecs_entity_t EcsAny =                         FLECS_HI_COMPONENT_ID + 11;
const ecs_entity_t EcsThis =                        FLECS_HI_COMPONENT_ID + 12;
const ecs_entity_t EcsVariable =                    FLECS_HI_COMPONENT_ID + 13;
const ecs_entity_t EcsTransitive =                  FLECS_HI_COMPONENT_ID + 14;
const ecs_entity_t EcsReflexive =                   FLECS_HI_COMPONENT_ID + 15;
const ecs_entity_t EcsSymmetric =                   FLECS_HI_COMPONENT_ID + 16;
const ecs_entity_t EcsFinal =                       FLECS_HI_COMPONENT_ID + 17;
const ecs_entity_t EcsDontInherit =                 FLECS_HI_COMPONENT_ID + 18;
const ecs_entity_t EcsAlwaysOverride =              FLECS_HI_COMPONENT_ID + 19;
const ecs_entity_t EcsTag =                         FLECS_HI_COMPONENT_ID + 20;
const ecs_entity_t EcsUnion =                       FLECS_HI_COMPONENT_ID + 21;
const ecs_entity_t EcsExclusive =                   FLECS_HI_COMPONENT_ID + 22;
const ecs_entity_t EcsAcyclic =                     FLECS_HI_COMPONENT_ID + 23;
const ecs_entity_t EcsTraversable =                 FLECS_HI_COMPONENT_ID + 24;
const ecs_entity_t EcsWith =                        FLECS_HI_COMPONENT_ID + 25;
const ecs_entity_t EcsOneOf =                       FLECS_HI_COMPONENT_ID + 26;

/* Builtin relationships */
const ecs_entity_t EcsChildOf =                     FLECS_HI_COMPONENT_ID + 27;
const ecs_entity_t EcsIsA =                         FLECS_HI_COMPONENT_ID + 28;
const ecs_entity_t EcsDependsOn =                   FLECS_HI_COMPONENT_ID + 29;

/* Identifier tags */
const ecs_entity_t EcsName =                        FLECS_HI_COMPONENT_ID + 30;
const ecs_entity_t EcsSymbol =                      FLECS_HI_COMPONENT_ID + 31;
const ecs_entity_t EcsAlias =                       FLECS_HI_COMPONENT_ID + 32;

/* Events */
const ecs_entity_t EcsOnAdd =                       FLECS_HI_COMPONENT_ID + 33;
const ecs_entity_t EcsOnRemove =                    FLECS_HI_COMPONENT_ID + 34;
const ecs_entity_t EcsOnSet =                       FLECS_HI_COMPONENT_ID + 35;
const ecs_entity_t EcsUnSet =                       FLECS_HI_COMPONENT_ID + 36;
const ecs_entity_t EcsOnDelete =                    FLECS_HI_COMPONENT_ID + 37;
const ecs_entity_t EcsOnTableCreate =               FLECS_HI_COMPONENT_ID + 38;
const ecs_entity_t EcsOnTableDelete =               FLECS_HI_COMPONENT_ID + 39;
const ecs_entity_t EcsOnTableEmpty =                FLECS_HI_COMPONENT_ID + 40;
const ecs_entity_t EcsOnTableFill =                 FLECS_HI_COMPONENT_ID + 41;
const ecs_entity_t EcsOnCreateTrigger =             FLECS_HI_COMPONENT_ID + 42;
const ecs_entity_t EcsOnDeleteTrigger =             FLECS_HI_COMPONENT_ID + 43;
const ecs_entity_t EcsOnDeleteObservable =          FLECS_HI_COMPONENT_ID + 44;
const ecs_entity_t EcsOnComponentHooks =            FLECS_HI_COMPONENT_ID + 45;
const ecs_entity_t EcsOnDeleteTarget =              FLECS_HI_COMPONENT_ID + 46;

/* Timers */
const ecs_entity_t ecs_id(EcsTickSource) =          FLECS_HI_COMPONENT_ID + 47;
const ecs_entity_t ecs_id(EcsTimer) =               FLECS_HI_COMPONENT_ID + 48;
const ecs_entity_t ecs_id(EcsRateFilter) =          FLECS_HI_COMPONENT_ID + 49;

/* Actions */
const ecs_entity_t EcsRemove =                      FLECS_HI_COMPONENT_ID + 50;
const ecs_entity_t EcsDelete =                      FLECS_HI_COMPONENT_ID + 51;
const ecs_entity_t EcsPanic =                       FLECS_HI_COMPONENT_ID + 52;

/* Misc */
const ecs_entity_t ecs_id(EcsTarget) =              FLECS_HI_COMPONENT_ID + 53;
const ecs_entity_t EcsFlatten =                     FLECS_HI_COMPONENT_ID + 54;
const ecs_entity_t EcsDefaultChildComponent =       FLECS_HI_COMPONENT_ID + 55;

/* Builtin predicate ids (used by rule engine) */
const ecs_entity_t EcsPredEq =                      FLECS_HI_COMPONENT_ID + 56;
const ecs_entity_t EcsPredMatch =                   FLECS_HI_COMPONENT_ID + 57;
const ecs_entity_t EcsPredLookup =                  FLECS_HI_COMPONENT_ID + 58;
const ecs_entity_t EcsScopeOpen =                    FLECS_HI_COMPONENT_ID + 59;
const ecs_entity_t EcsScopeClose =                   FLECS_HI_COMPONENT_ID + 60;

/* Systems */
const ecs_entity_t EcsMonitor =                     FLECS_HI_COMPONENT_ID + 61;
const ecs_entity_t EcsEmpty =                       FLECS_HI_COMPONENT_ID + 62;
const ecs_entity_t ecs_id(EcsPipeline) =            FLECS_HI_COMPONENT_ID + 63;
const ecs_entity_t EcsOnStart =                     FLECS_HI_COMPONENT_ID + 64;
const ecs_entity_t EcsPreFrame =                    FLECS_HI_COMPONENT_ID + 65;
const ecs_entity_t EcsOnLoad =                      FLECS_HI_COMPONENT_ID + 66;
const ecs_entity_t EcsPostLoad =                    FLECS_HI_COMPONENT_ID + 67;
const ecs_entity_t EcsPreUpdate =                   FLECS_HI_COMPONENT_ID + 68;
const ecs_entity_t EcsOnUpdate =                    FLECS_HI_COMPONENT_ID + 69;
const ecs_entity_t EcsOnValidate =                  FLECS_HI_COMPONENT_ID + 70;
const ecs_entity_t EcsPostUpdate =                  FLECS_HI_COMPONENT_ID + 71;
const ecs_entity_t EcsPreStore =                    FLECS_HI_COMPONENT_ID + 72;
const ecs_entity_t EcsOnStore =                     FLECS_HI_COMPONENT_ID + 73;
const ecs_entity_t EcsPostFrame =                   FLECS_HI_COMPONENT_ID + 74;
const ecs_entity_t EcsPhase =                       FLECS_HI_COMPONENT_ID + 75;

/* Meta primitive components (don't use low ids to save id space) */
const ecs_entity_t ecs_id(ecs_bool_t) =             FLECS_HI_COMPONENT_ID + 80;
const ecs_entity_t ecs_id(ecs_char_t) =             FLECS_HI_COMPONENT_ID + 81;
const ecs_entity_t ecs_id(ecs_byte_t) =             FLECS_HI_COMPONENT_ID + 82;
const ecs_entity_t ecs_id(ecs_u8_t) =               FLECS_HI_COMPONENT_ID + 83;
const ecs_entity_t ecs_id(ecs_u16_t) =              FLECS_HI_COMPONENT_ID + 84;
const ecs_entity_t ecs_id(ecs_u32_t) =              FLECS_HI_COMPONENT_ID + 85;
const ecs_entity_t ecs_id(ecs_u64_t) =              FLECS_HI_COMPONENT_ID + 86;
const ecs_entity_t ecs_id(ecs_uptr_t) =             FLECS_HI_COMPONENT_ID + 87;
const ecs_entity_t ecs_id(ecs_i8_t) =               FLECS_HI_COMPONENT_ID + 88;
const ecs_entity_t ecs_id(ecs_i16_t) =              FLECS_HI_COMPONENT_ID + 89;
const ecs_entity_t ecs_id(ecs_i32_t) =              FLECS_HI_COMPONENT_ID + 90;
const ecs_entity_t ecs_id(ecs_i64_t) =              FLECS_HI_COMPONENT_ID + 91;
const ecs_entity_t ecs_id(ecs_iptr_t) =             FLECS_HI_COMPONENT_ID + 92;
const ecs_entity_t ecs_id(ecs_f32_t) =              FLECS_HI_COMPONENT_ID + 93;
const ecs_entity_t ecs_id(ecs_f64_t) =              FLECS_HI_COMPONENT_ID + 94;
const ecs_entity_t ecs_id(ecs_string_t) =           FLECS_HI_COMPONENT_ID + 95;
const ecs_entity_t ecs_id(ecs_entity_t) =           FLECS_HI_COMPONENT_ID + 96;

/** Meta module component ids */
const ecs_entity_t ecs_id(EcsMetaType) =            FLECS_HI_COMPONENT_ID + 97;
const ecs_entity_t ecs_id(EcsMetaTypeSerialized) =  FLECS_HI_COMPONENT_ID + 98;
const ecs_entity_t ecs_id(EcsPrimitive) =           FLECS_HI_COMPONENT_ID + 99;
const ecs_entity_t ecs_id(EcsEnum) =                FLECS_HI_COMPONENT_ID + 100;
const ecs_entity_t ecs_id(EcsBitmask) =             FLECS_HI_COMPONENT_ID + 101;
const ecs_entity_t ecs_id(EcsMember) =              FLECS_HI_COMPONENT_ID + 102;
const ecs_entity_t ecs_id(EcsStruct) =              FLECS_HI_COMPONENT_ID + 103;
const ecs_entity_t ecs_id(EcsArray) =               FLECS_HI_COMPONENT_ID + 104;
const ecs_entity_t ecs_id(EcsVector) =              FLECS_HI_COMPONENT_ID + 105;
const ecs_entity_t ecs_id(EcsOpaque) =              FLECS_HI_COMPONENT_ID + 106;
const ecs_entity_t ecs_id(EcsUnit) =                FLECS_HI_COMPONENT_ID + 107;
const ecs_entity_t ecs_id(EcsUnitPrefix) =          FLECS_HI_COMPONENT_ID + 108;
const ecs_entity_t EcsConstant =                    FLECS_HI_COMPONENT_ID + 109;
const ecs_entity_t EcsQuantity =                    FLECS_HI_COMPONENT_ID + 110;

/* Doc module components */
const ecs_entity_t ecs_id(EcsDocDescription) =      FLECS_HI_COMPONENT_ID + 111;
const ecs_entity_t EcsDocBrief =                    FLECS_HI_COMPONENT_ID + 112;
const ecs_entity_t EcsDocDetail =                   FLECS_HI_COMPONENT_ID + 113;
const ecs_entity_t EcsDocLink =                     FLECS_HI_COMPONENT_ID + 114;
const ecs_entity_t EcsDocColor =                    FLECS_HI_COMPONENT_ID + 115;

/* REST module components */
const ecs_entity_t ecs_id(EcsRest) =                FLECS_HI_COMPONENT_ID + 116;

/* Default lookup path */
static ecs_entity_t ecs_default_lookup_path[2] = { 0, 0 };

/* Declarations for addons. Located in world.c to avoid issues during linking of 
 * static library */
#ifdef FLECS_ALERTS
ECS_COMPONENT_DECLARE(EcsAlert);
ECS_COMPONENT_DECLARE(EcsAlertInstance);
ECS_COMPONENT_DECLARE(EcsAlertsActive);
#endif

/* -- Private functions -- */

const ecs_stage_t* flecs_stage_from_readonly_world(
    const ecs_world_t *world)
{
    ecs_assert(ecs_poly_is(world, ecs_world_t) ||
               ecs_poly_is(world, ecs_stage_t),
               ECS_INTERNAL_ERROR,
               NULL);

    if (ecs_poly_is(world, ecs_world_t)) {
        return &world->stages[0];

    } else if (ecs_poly_is(world, ecs_stage_t)) {
        return (ecs_stage_t*)world;
    }
    
    return NULL;    
}

ecs_stage_t* flecs_stage_from_world(
    ecs_world_t **world_ptr)
{
    ecs_world_t *world = *world_ptr;

    ecs_assert(ecs_poly_is(world, ecs_world_t) ||
               ecs_poly_is(world, ecs_stage_t),
               ECS_INTERNAL_ERROR,
               NULL);

    if (ecs_poly_is(world, ecs_world_t)) {
        return &world->stages[0];
    }

    *world_ptr = ((ecs_stage_t*)world)->world;
    return (ecs_stage_t*)world;
}

ecs_world_t* flecs_suspend_readonly(
    const ecs_world_t *stage_world,
    ecs_suspend_readonly_state_t *state)
{
    ecs_assert(stage_world != NULL, ECS_INTERNAL_ERROR, NULL);
    ecs_assert(state != NULL, ECS_INTERNAL_ERROR, NULL);

    ecs_world_t *world = (ecs_world_t*)ecs_get_world(stage_world);
    ecs_poly_assert(world, ecs_world_t);

    bool is_readonly = ECS_BIT_IS_SET(world->flags, EcsWorldReadonly);
    bool is_deferred = ecs_is_deferred(world);

    if (!is_readonly && !is_deferred) {
        state->is_readonly = false;
        state->is_deferred = false;
        return world;
    }

    ecs_dbg_3("suspending readonly mode");

    /* Cannot suspend when running with multiple threads */
    ecs_assert(!(world->flags & EcsWorldReadonly) ||
        (ecs_get_stage_count(world) <= 1), ECS_INVALID_WHILE_READONLY, NULL);

    state->is_readonly = is_readonly;
    state->is_deferred = is_deferred;

    /* Silence readonly checks */
    world->flags &= ~EcsWorldReadonly;

    /* Hack around safety checks (this ought to look ugly) */
    ecs_world_t *temp_world = world;
    ecs_stage_t *stage = flecs_stage_from_world(&temp_world);
    state->defer_count = stage->defer;
    state->commands = stage->commands;
    state->defer_stack = stage->defer_stack;
    flecs_stack_init(&stage->defer_stack);
    state->scope = stage->scope;
    state->with = stage->with;
    stage->defer = 0;
    ecs_vec_init_t(NULL, &stage->commands, ecs_cmd_t, 0);
    
    return world;
}

void flecs_resume_readonly(
    ecs_world_t *world,
    ecs_suspend_readonly_state_t *state)
{
    ecs_poly_assert(world, ecs_world_t);
    ecs_assert(state != NULL, ECS_INTERNAL_ERROR, NULL);
    
    ecs_world_t *temp_world = world;
    ecs_stage_t *stage = flecs_stage_from_world(&temp_world);

    if (state->is_readonly || state->is_deferred) {
        ecs_dbg_3("resuming readonly mode");
        
        ecs_run_aperiodic(world, 0);

        /* Restore readonly state / defer count */
        ECS_BIT_COND(world->flags, EcsWorldReadonly, state->is_readonly);
        stage->defer = state->defer_count;
        ecs_vec_fini_t(&stage->allocator, &stage->commands, ecs_cmd_t);
        stage->commands = state->commands;
        flecs_stack_fini(&stage->defer_stack);
        stage->defer_stack = state->defer_stack;
        stage->scope = state->scope;
        stage->with = state->with;
    }
}

/* Evaluate component monitor. If a monitored entity changed it will have set a
 * flag in one of the world's component monitors. Queries can register 
 * themselves with component monitors to determine whether they need to rematch
 * with tables. */
static
void flecs_eval_component_monitor(
    ecs_world_t *world)
{
    ecs_poly_assert(world, ecs_world_t);

    if (!world->monitors.is_dirty) {
        return;
    }

    world->monitors.is_dirty = false;

    ecs_map_iter_t it = ecs_map_iter(&world->monitors.monitors);
    while (ecs_map_next(&it)) {
        ecs_monitor_t *m = ecs_map_ptr(&it);
        if (!m->is_dirty) {
            continue;
        }

        m->is_dirty = false;

        int32_t i, count = ecs_vec_count(&m->queries);
        ecs_query_t **elems = ecs_vec_first(&m->queries);
        for (i = 0; i < count; i ++) {
            ecs_query_t *q = elems[i];
            flecs_query_notify(world, q, &(ecs_query_event_t) {
                .kind = EcsQueryTableRematch
            });
        }
    }
}

void flecs_monitor_mark_dirty(
    ecs_world_t *world,
    ecs_entity_t id)
{
    ecs_map_t *monitors = &world->monitors.monitors;

    /* Only flag if there are actually monitors registered, so that we
     * don't waste cycles evaluating monitors if there's no interest */
    if (ecs_map_is_init(monitors)) {
        ecs_monitor_t *m = ecs_map_get_deref(monitors, ecs_monitor_t, id);
        if (m) {
            if (!world->monitors.is_dirty) {
                world->monitor_generation ++;
            }
            m->is_dirty = true;
            world->monitors.is_dirty = true;
        }
    }
}

void flecs_monitor_register(
    ecs_world_t *world,
    ecs_entity_t id,
    ecs_query_t *query)
{
    ecs_assert(world != NULL, ECS_INTERNAL_ERROR, NULL);
    ecs_assert(id != 0, ECS_INTERNAL_ERROR, NULL);
    ecs_assert(query != NULL, ECS_INTERNAL_ERROR, NULL);

    ecs_map_t *monitors = &world->monitors.monitors;
    ecs_map_init_if(monitors, &world->allocator);
    ecs_monitor_t *m = ecs_map_ensure_alloc_t(monitors, ecs_monitor_t, id);
    ecs_vec_init_if_t(&m->queries, ecs_query_t*);
    ecs_query_t **q = ecs_vec_append_t(
        &world->allocator, &m->queries, ecs_query_t*);
    *q = query;
}

void flecs_monitor_unregister(
    ecs_world_t *world,
    ecs_entity_t id,
    ecs_query_t *query)
{
    ecs_assert(world != NULL, ECS_INTERNAL_ERROR, NULL);
    ecs_assert(id != 0, ECS_INTERNAL_ERROR, NULL);
    ecs_assert(query != NULL, ECS_INTERNAL_ERROR, NULL);

    ecs_map_t *monitors = &world->monitors.monitors;
    if (!ecs_map_is_init(monitors)) {
        return;
    }

    ecs_monitor_t *m = ecs_map_get_deref(monitors, ecs_monitor_t, id);
    if (!m) {
        return;
    }

    int32_t i, count = ecs_vec_count(&m->queries);
    ecs_query_t **queries = ecs_vec_first(&m->queries);
    for (i = 0; i < count; i ++) {
        if (queries[i] == query) {
            ecs_vec_remove_t(&m->queries, ecs_query_t*, i);
            count --;
            break;
        }
    }

    if (!count) {
        ecs_vec_fini_t(&world->allocator, &m->queries, ecs_query_t*);
        ecs_map_remove_free(monitors, id);
    }

    if (!ecs_map_count(monitors)) {
        ecs_map_fini(monitors);
    }
}

static
void flecs_init_store(
    ecs_world_t *world) 
{
    ecs_os_memset(&world->store, 0, ECS_SIZEOF(ecs_store_t));
    
    ecs_allocator_t *a = &world->allocator;
    ecs_vec_init_t(a, &world->store.records, ecs_table_record_t, 0);
    ecs_vec_init_t(a, &world->store.marked_ids, ecs_marked_id_t, 0);
    ecs_vec_init_t(a, &world->store.depth_ids, ecs_entity_t, 0);
    ecs_map_init(&world->store.entity_to_depth, &world->allocator);

    /* Initialize entity index */
    flecs_entities_init(world);

    /* Initialize root table */
    flecs_sparse_init_t(&world->store.tables, 
        a, &world->allocators.sparse_chunk, ecs_table_t);

    /* Initialize table map */
    flecs_table_hashmap_init(world, &world->store.table_map);

    /* Initialize one root table per stage */
    flecs_init_root_table(world);
}

static
void flecs_clean_tables(
    ecs_world_t *world)
{
    int32_t i, count = flecs_sparse_count(&world->store.tables);

    /* Ensure that first table in sparse set has id 0. This is a dummy table
     * that only exists so that there is no table with id 0 */
    ecs_table_t *first = flecs_sparse_get_dense_t(&world->store.tables, 
        ecs_table_t, 0);
    (void)first;

    for (i = 1; i < count; i ++) {
        ecs_table_t *t = flecs_sparse_get_dense_t(&world->store.tables, 
            ecs_table_t, i);
        flecs_table_release(world, t);
    }

    /* Free table types separately so that if application destructors rely on
     * a type it's still valid. */
    for (i = 1; i < count; i ++) {
        ecs_table_t *t = flecs_sparse_get_dense_t(&world->store.tables, 
            ecs_table_t, i);
        flecs_table_free_type(world, t);
    }

    /* Clear the root table */
    if (count) {
        flecs_table_reset(world, &world->store.root);
    }
}

static
void flecs_fini_roots(ecs_world_t *world) {
    ecs_id_record_t *idr = flecs_id_record_get(world, ecs_pair(EcsChildOf, 0));

    ecs_run_aperiodic(world, EcsAperiodicEmptyTables);

    ecs_table_cache_iter_t it;
    bool has_roots = flecs_table_cache_iter(&idr->cache, &it);
    ecs_assert(has_roots == true, ECS_INTERNAL_ERROR, NULL);
    (void)has_roots;

    /* Delete root entities that are not modules. This prioritizes deleting 
     * regular entities first, which reduces the chance of components getting
     * destructed in random order because it got deleted before entities,
     * thereby bypassing the OnDeleteTarget policy. */
    flecs_defer_begin(world, &world->stages[0]);

    const ecs_table_record_t *tr;
    while ((tr = flecs_table_cache_next(&it, ecs_table_record_t))) {
        ecs_table_t *table = tr->hdr.table;
        if (table->flags & EcsTableHasBuiltins) {
            continue; /* Filter out modules */
        }

        int32_t i, count = table->data.entities.count;
        ecs_entity_t *entities = table->data.entities.array;

        /* Count backwards so that we're always deleting the last entity in the
         * table which reduces moving components around */
        for (i = count - 1; i >= 0; i --) {
            ecs_record_t *r = flecs_entities_get(world, entities[i]);
            ecs_assert(r != NULL, ECS_INTERNAL_ERROR, NULL);

            ecs_flags32_t flags = ECS_RECORD_TO_ROW_FLAGS(r->row);
            if (!(flags & EcsEntityIsTarget)) {
                continue; /* Filter out entities that aren't objects */
            }

            ecs_delete(world, entities[i]);
        }
    }

    flecs_defer_end(world, &world->stages[0]);
}

static
void flecs_fini_store(ecs_world_t *world) {
    flecs_clean_tables(world);
    flecs_sparse_fini(&world->store.tables);
    flecs_table_release(world, &world->store.root);
    flecs_entities_clear(world);
    flecs_hashmap_fini(&world->store.table_map);

    ecs_allocator_t *a = &world->allocator;
    ecs_vec_fini_t(a, &world->store.records, ecs_table_record_t);
    ecs_vec_fini_t(a, &world->store.marked_ids, ecs_marked_id_t);
    ecs_vec_fini_t(a, &world->store.depth_ids, ecs_entity_t);
    ecs_map_fini(&world->store.entity_to_depth);
}

/* Implementation for iterable mixin */
static
bool flecs_world_iter_next(
    ecs_iter_t *it)
{
    if (ECS_BIT_IS_SET(it->flags, EcsIterIsValid)) {
        ECS_BIT_CLEAR(it->flags, EcsIterIsValid);
        ecs_iter_fini(it);
        return false;
    }

    ecs_world_t *world = it->real_world;
    it->entities = (ecs_entity_t*)flecs_entities_ids(world);
    it->count = flecs_entities_count(world);
    flecs_iter_validate(it);

    return true;
}

static
void flecs_world_iter_init(
    const ecs_world_t *world,
    const ecs_poly_t *poly,
    ecs_iter_t *iter,
    ecs_term_t *filter)
{
    ecs_poly_assert(poly, ecs_world_t);
    (void)poly;

    if (filter) {
        iter[0] = ecs_term_iter(world, filter);
    } else {
        iter[0] = (ecs_iter_t){
            .world = (ecs_world_t*)world,
            .real_world = (ecs_world_t*)ecs_get_world(world),
            .next = flecs_world_iter_next
        };
    }
}

static 
void flecs_world_allocators_init(
    ecs_world_t *world)
{
    ecs_world_allocators_t *a = &world->allocators;

    flecs_allocator_init(&world->allocator);

    ecs_map_params_init(&a->ptr, &world->allocator);
    ecs_map_params_init(&a->query_table_list, &world->allocator);

    flecs_ballocator_init_t(&a->query_table, ecs_query_table_t);
    flecs_ballocator_init_t(&a->query_table_match, ecs_query_table_match_t);
    flecs_ballocator_init_n(&a->graph_edge_lo, ecs_graph_edge_t, FLECS_HI_COMPONENT_ID);
    flecs_ballocator_init_t(&a->graph_edge, ecs_graph_edge_t);
    flecs_ballocator_init_t(&a->id_record, ecs_id_record_t);
    flecs_ballocator_init_n(&a->id_record_chunk, ecs_id_record_t, FLECS_SPARSE_PAGE_SIZE);
    flecs_ballocator_init_t(&a->table_diff, ecs_table_diff_t);
    flecs_ballocator_init_n(&a->sparse_chunk, int32_t, FLECS_SPARSE_PAGE_SIZE);
    flecs_ballocator_init_t(&a->hashmap, ecs_hashmap_t);
    flecs_table_diff_builder_init(world, &world->allocators.diff_builder);
}

static 
void flecs_world_allocators_fini(
    ecs_world_t *world)
{
    ecs_world_allocators_t *a = &world->allocators;

    ecs_map_params_fini(&a->ptr);
    ecs_map_params_fini(&a->query_table_list);

    flecs_ballocator_fini(&a->query_table);
    flecs_ballocator_fini(&a->query_table_match);
    flecs_ballocator_fini(&a->graph_edge_lo);
    flecs_ballocator_fini(&a->graph_edge);
    flecs_ballocator_fini(&a->id_record);
    flecs_ballocator_fini(&a->id_record_chunk);
    flecs_ballocator_fini(&a->table_diff);
    flecs_ballocator_fini(&a->sparse_chunk);
    flecs_ballocator_fini(&a->hashmap);
    flecs_table_diff_builder_fini(world, &world->allocators.diff_builder);

    flecs_allocator_fini(&world->allocator);
}

static
void flecs_log_addons(void) {
    ecs_trace("addons included in build:");
    ecs_log_push();
    #ifdef FLECS_CPP
        ecs_trace("FLECS_CPP");
    #endif
    #ifdef FLECS_MODULE
        ecs_trace("FLECS_MODULE");
    #endif
    #ifdef FLECS_PARSER
        ecs_trace("FLECS_PARSER");
    #endif
    #ifdef FLECS_PLECS
        ecs_trace("FLECS_PLECS");
    #endif
    #ifdef FLECS_RULES
        ecs_trace("FLECS_RULES");
    #endif
    #ifdef FLECS_SNAPSHOT
        ecs_trace("FLECS_SNAPSHOT");
    #endif
    #ifdef FLECS_STATS
        ecs_trace("FLECS_STATS");
    #endif
    #ifdef FLECS_MONITOR
        ecs_trace("FLECS_MONITOR");
    #endif
    #ifdef FLECS_METRICS
        ecs_trace("FLECS_METRICS");
    #endif
    #ifdef FLECS_SYSTEM
        ecs_trace("FLECS_SYSTEM");
    #endif
    #ifdef FLECS_PIPELINE
        ecs_trace("FLECS_PIPELINE");
    #endif
    #ifdef FLECS_TIMER
        ecs_trace("FLECS_TIMER");
    #endif
    #ifdef FLECS_META
        ecs_trace("FLECS_META");
    #endif
    #ifdef FLECS_META_C
        ecs_trace("FLECS_META_C");
    #endif
    #ifdef FLECS_UNITS
        ecs_trace("FLECS_UNITS");
    #endif
    #ifdef FLECS_EXPR
        ecs_trace("FLECS_EXPR");
    #endif
    #ifdef FLECS_JSON
        ecs_trace("FLECS_JSON");
    #endif
    #ifdef FLECS_DOC
        ecs_trace("FLECS_DOC");
    #endif
    #ifdef FLECS_COREDOC
        ecs_trace("FLECS_COREDOC");
    #endif
    #ifdef FLECS_LOG
        ecs_trace("FLECS_LOG");
    #endif
    #ifdef FLECS_JOURNAL
        ecs_trace("FLECS_JOURNAL");
    #endif
    #ifdef FLECS_APP
        ecs_trace("FLECS_APP");
    #endif
    #ifdef FLECS_OS_API_IMPL
        ecs_trace("FLECS_OS_API_IMPL");
    #endif
    #ifdef FLECS_SCRIPT
        ecs_trace("FLECS_SCRIPT");
    #endif
    #ifdef FLECS_HTTP
        ecs_trace("FLECS_HTTP");
    #endif
    #ifdef FLECS_REST
        ecs_trace("FLECS_REST");
    #endif
    ecs_log_pop();
}

/* -- Public functions -- */

ecs_world_t *ecs_mini(void) {
#ifdef FLECS_OS_API_IMPL
    ecs_set_os_api_impl();
#endif
    ecs_os_init();

    ecs_trace("#[bold]bootstrapping world");
    ecs_log_push();

    ecs_trace("tracing enabled, call ecs_log_set_level(-1) to disable");

    if (!ecs_os_has_heap()) {
        ecs_abort(ECS_MISSING_OS_API, NULL);
    }

    if (!ecs_os_has_threading()) {
        ecs_trace("threading unavailable, to use threads set OS API first (see examples)");
    }

    if (!ecs_os_has_time()) {
        ecs_trace("time management not available");
    }

    flecs_log_addons();

#ifdef FLECS_SANITIZE
    ecs_trace("sanitize build, rebuild without FLECS_SANITIZE for (much) "
        "improved performance");
#elif defined(FLECS_DEBUG)
    ecs_trace("debug build, rebuild with NDEBUG or FLECS_NDEBUG for improved "
        "performance");
#else
    ecs_trace("#[green]release#[reset] build");
#endif

#ifdef __clang__
    ecs_trace("compiled with clang %s", __clang_version__);
#elif defined(__GNUC__)
    ecs_trace("compiled with gcc %d.%d", __GNUC__, __GNUC_MINOR__);
#elif defined (_MSC_VER)
    ecs_trace("compiled with msvc %d", _MSC_VER);
#elif defined (__TINYC__)
    ecs_trace("compiled with tcc %d", __TINYC__);
#endif

    ecs_world_t *world = ecs_os_calloc_t(ecs_world_t);
    ecs_assert(world != NULL, ECS_OUT_OF_MEMORY, NULL);
    ecs_poly_init(world, ecs_world_t);

    world->flags |= EcsWorldInit;

    flecs_world_allocators_init(world);
    ecs_allocator_t *a = &world->allocator;

    world->self = world;
    flecs_sparse_init_t(&world->type_info, a,
        &world->allocators.sparse_chunk, ecs_type_info_t);
    ecs_map_init_w_params(&world->id_index_hi, &world->allocators.ptr);
    world->id_index_lo = ecs_os_calloc_n(ecs_id_record_t, FLECS_HI_ID_RECORD_ID);
    flecs_observable_init(&world->observable);
    world->iterable.init = flecs_world_iter_init;

    world->pending_tables = ecs_os_calloc_t(ecs_sparse_t);
    flecs_sparse_init_t(world->pending_tables, a, 
        &world->allocators.sparse_chunk, ecs_table_t*);
    world->pending_buffer = ecs_os_calloc_t(ecs_sparse_t);
    flecs_sparse_init_t(world->pending_buffer, a,
        &world->allocators.sparse_chunk, ecs_table_t*);

    flecs_name_index_init(&world->aliases, a);
    flecs_name_index_init(&world->symbols, a);
    ecs_vec_init_t(a, &world->fini_actions, ecs_action_elem_t, 0);

    world->info.time_scale = 1.0;
    if (ecs_os_has_time()) {
        ecs_os_get_time(&world->world_start_time);
    }

    ecs_set_stage_count(world, 1);
    ecs_default_lookup_path[0] = EcsFlecsCore;
    ecs_set_lookup_path(world, ecs_default_lookup_path);
    flecs_init_store(world);

    flecs_bootstrap(world);

    world->flags &= ~EcsWorldInit;

    ecs_trace("world ready!");
    ecs_log_pop();

    return world;
}

ecs_world_t *ecs_init(void) {
    ecs_world_t *world = ecs_mini();

#ifdef FLECS_MODULE_H
    ecs_trace("#[bold]import addons");
    ecs_log_push();
    ecs_trace("use ecs_mini to create world without importing addons");
#ifdef FLECS_SYSTEM
    ECS_IMPORT(world, FlecsSystem);
#endif
#ifdef FLECS_PIPELINE
    ECS_IMPORT(world, FlecsPipeline);
#endif
#ifdef FLECS_TIMER
    ECS_IMPORT(world, FlecsTimer);
#endif
#ifdef FLECS_META
    ECS_IMPORT(world, FlecsMeta);
#endif
#ifdef FLECS_DOC
    ECS_IMPORT(world, FlecsDoc);
#endif
#ifdef FLECS_COREDOC
    ECS_IMPORT(world, FlecsCoreDoc);
#endif
#ifdef FLECS_SCRIPT
    ECS_IMPORT(world, FlecsScript);
#endif
#ifdef FLECS_REST
    ECS_IMPORT(world, FlecsRest);
#endif
#ifdef FLECS_UNITS
    ecs_trace("#[green]module#[reset] flecs.units is not automatically imported");
#endif
    ecs_trace("addons imported!");
    ecs_log_pop();
#endif
    return world;
}

#define ARG(short, long, action)\
    if (i < argc) {\
        if (argv[i][0] == '-') {\
            if (argv[i][1] == '-') {\
                if (long && !strcmp(&argv[i][2], long ? long : "")) {\
                    action;\
                    parsed = true;\
                }\
            } else {\
                if (short && argv[i][1] == short) {\
                    action;\
                    parsed = true;\
                }\
            }\
        }\
    }

ecs_world_t* ecs_init_w_args(
    int argc,
    char *argv[])
{
    ecs_world_t *world = ecs_init();

    (void)argc;
    (void) argv;

#ifdef FLECS_DOC
    if (argc) {
        char *app = argv[0];
        char *last_elem = strrchr(app, '/');
        if (!last_elem) {
            last_elem = strrchr(app, '\\');
        }
        if (last_elem) {
            app = last_elem + 1;
        }
        ecs_set_pair(world, EcsWorld, EcsDocDescription, EcsName, {app});
    }
#endif

    return world;
}

void ecs_quit(
    ecs_world_t *world)
{
    ecs_check(world != NULL, ECS_INVALID_PARAMETER, NULL);
    flecs_stage_from_world(&world);
    world->flags |= EcsWorldQuit;
error:
    return;
}

bool ecs_should_quit(
    const ecs_world_t *world)
{
    ecs_check(world != NULL, ECS_INVALID_PARAMETER, NULL);
    world = ecs_get_world(world);
    return ECS_BIT_IS_SET(world->flags, EcsWorldQuit);
error:
    return true;
}

void flecs_notify_tables(
    ecs_world_t *world,
    ecs_id_t id,
    ecs_table_event_t *event)
{
    ecs_poly_assert(world, ecs_world_t);

    /* If no id is specified, broadcast to all tables */
    if (!id) {
        ecs_sparse_t *tables = &world->store.tables;
        int32_t i, count = flecs_sparse_count(tables);
        for (i = 0; i < count; i ++) {
            ecs_table_t *table = flecs_sparse_get_dense_t(tables, ecs_table_t, i);
            flecs_table_notify(world, table, event);
        }

    /* If id is specified, only broadcast to tables with id */
    } else {
        ecs_id_record_t *idr = flecs_id_record_get(world, id);
        if (!idr) {
            return;
        }

        ecs_table_cache_iter_t it;
        const ecs_table_record_t *tr;

        flecs_table_cache_all_iter(&idr->cache, &it);
        while ((tr = flecs_table_cache_next(&it, ecs_table_record_t))) {
            flecs_table_notify(world, tr->hdr.table, event);
        }
    }
}

void ecs_default_ctor(
    void *ptr, 
    int32_t count, 
    const ecs_type_info_t *ti)
{
    ecs_os_memset(ptr, 0, ti->size * count);
}

static
void flecs_default_copy_ctor(void *dst_ptr, const void *src_ptr,
    int32_t count, const ecs_type_info_t *ti)
{
    const ecs_type_hooks_t *cl = &ti->hooks;
    cl->ctor(dst_ptr, count, ti);
    cl->copy(dst_ptr, src_ptr, count, ti);
}

static
void flecs_default_move_ctor(void *dst_ptr, void *src_ptr,
    int32_t count, const ecs_type_info_t *ti)
{
    const ecs_type_hooks_t *cl = &ti->hooks;
    cl->ctor(dst_ptr, count, ti);
    cl->move(dst_ptr, src_ptr, count, ti);
}

static
void flecs_default_ctor_w_move_w_dtor(void *dst_ptr, void *src_ptr,
    int32_t count, const ecs_type_info_t *ti)
{
    const ecs_type_hooks_t *cl = &ti->hooks;
    cl->ctor(dst_ptr, count, ti);
    cl->move(dst_ptr, src_ptr, count, ti);
    cl->dtor(src_ptr, count, ti);
}

static
void flecs_default_move_ctor_w_dtor(void *dst_ptr, void *src_ptr,
    int32_t count, const ecs_type_info_t *ti)
{
    const ecs_type_hooks_t *cl = &ti->hooks;
    cl->move_ctor(dst_ptr, src_ptr, count, ti);
    cl->dtor(src_ptr, count, ti);
}

static
void flecs_default_move(void *dst_ptr, void *src_ptr,
    int32_t count, const ecs_type_info_t *ti)
{
    const ecs_type_hooks_t *cl = &ti->hooks;
    cl->move(dst_ptr, src_ptr, count, ti);
}

static
void flecs_default_dtor(void *dst_ptr, void *src_ptr,
    int32_t count, const ecs_type_info_t *ti)
{
    /* When there is no move, destruct the destination component & memcpy the
     * component to dst. The src component does not have to be destructed when
     * a component has a trivial move. */
    const ecs_type_hooks_t *cl = &ti->hooks;
    cl->dtor(dst_ptr, count, ti);
    ecs_os_memcpy(dst_ptr, src_ptr, flecs_uto(ecs_size_t, ti->size) * count);
}

static
void flecs_default_move_w_dtor(void *dst_ptr, void *src_ptr,
    int32_t count, const ecs_type_info_t *ti)
{
    /* If a component has a move, the move will take care of memcpying the data
     * and destroying any data in dst. Because this is not a trivial move, the
     * src component must also be destructed. */
    const ecs_type_hooks_t *cl = &ti->hooks;
    cl->move(dst_ptr, src_ptr, count, ti);
    cl->dtor(src_ptr, count, ti);
}

void ecs_set_hooks_id(
    ecs_world_t *world,
    ecs_entity_t component,
    const ecs_type_hooks_t *h)
{
    ecs_check(world != NULL, ECS_INVALID_PARAMETER, NULL);

    flecs_stage_from_world(&world);

    /* Ensure that no tables have yet been created for the component */
    ecs_assert( ecs_id_in_use(world, component) == false, 
        ECS_ALREADY_IN_USE, ecs_get_name(world, component));
    ecs_assert( ecs_id_in_use(world, ecs_pair(component, EcsWildcard)) == false, 
        ECS_ALREADY_IN_USE, ecs_get_name(world, component));

    ecs_type_info_t *ti = flecs_type_info_ensure(world, component);
    ecs_assert(ti != NULL, ECS_INTERNAL_ERROR, NULL);

    ecs_check(!ti->component || ti->component == component, 
        ECS_INCONSISTENT_COMPONENT_ACTION, NULL);

    if (!ti->size) {
        const EcsComponent *component_ptr = ecs_get(
            world, component, EcsComponent);

        /* Cannot register lifecycle actions for things that aren't a component */
        ecs_check(component_ptr != NULL, ECS_INVALID_PARAMETER, NULL);
        /* Cannot register lifecycle actions for components with size 0 */
        ecs_check(component_ptr->size != 0, ECS_INVALID_PARAMETER, NULL);

        ti->size = component_ptr->size;
        ti->alignment = component_ptr->alignment;
    }

    if (h->ctor) ti->hooks.ctor = h->ctor;
    if (h->dtor) ti->hooks.dtor = h->dtor;
    if (h->copy) ti->hooks.copy = h->copy;
    if (h->move) ti->hooks.move = h->move;
    if (h->copy_ctor) ti->hooks.copy_ctor = h->copy_ctor;
    if (h->move_ctor) ti->hooks.move_ctor = h->move_ctor;
    if (h->ctor_move_dtor) ti->hooks.ctor_move_dtor = h->ctor_move_dtor;
    if (h->move_dtor) ti->hooks.move_dtor = h->move_dtor;

    if (h->on_add) ti->hooks.on_add = h->on_add;
    if (h->on_remove) ti->hooks.on_remove = h->on_remove;
    if (h->on_set) ti->hooks.on_set = h->on_set;

    if (h->ctx) ti->hooks.ctx = h->ctx;
    if (h->binding_ctx) ti->hooks.binding_ctx = h->binding_ctx;
    if (h->ctx_free) ti->hooks.ctx_free = h->ctx_free;
    if (h->binding_ctx_free) ti->hooks.binding_ctx_free = h->binding_ctx_free;

    /* If no constructor is set, invoking any of the other lifecycle actions 
     * is not safe as they will potentially access uninitialized memory. For 
     * ease of use, if no constructor is specified, set a default one that 
     * initializes the component to 0. */
    if (!h->ctor && (h->dtor || h->copy || h->move)) {
        ti->hooks.ctor = ecs_default_ctor;   
    }

    /* Set default copy ctor, move ctor and merge */
    if (h->copy && !h->copy_ctor) {
        ti->hooks.copy_ctor = flecs_default_copy_ctor;
    }

    if (h->move && !h->move_ctor) {
        ti->hooks.move_ctor = flecs_default_move_ctor;
    }

    if (!h->ctor_move_dtor) {
        if (h->move) {
            if (h->dtor) {
                if (h->move_ctor) {
                    /* If an explicit move ctor has been set, use callback 
                     * that uses the move ctor vs. using a ctor+move */
                    ti->hooks.ctor_move_dtor = flecs_default_move_ctor_w_dtor;
                } else {
                    /* If no explicit move_ctor has been set, use
                     * combination of ctor + move + dtor */
                    ti->hooks.ctor_move_dtor = flecs_default_ctor_w_move_w_dtor;
                }
            } else {
                /* If no dtor has been set, this is just a move ctor */
                ti->hooks.ctor_move_dtor = ti->hooks.move_ctor;
            }            
        } else {
            /* If move is not set but move_ctor and dtor is, we can still set
             * ctor_move_dtor. */
            if (h->move_ctor) {
                if (h->dtor) {
                    ti->hooks.ctor_move_dtor = flecs_default_move_ctor_w_dtor;
                } else {
                    ti->hooks.ctor_move_dtor = ti->hooks.move_ctor;
                }
            }
        }
    }

    if (!h->move_dtor) {
        if (h->move) {
            if (h->dtor) {
                ti->hooks.move_dtor = flecs_default_move_w_dtor;
            } else {
                ti->hooks.move_dtor = flecs_default_move;
            }
        } else {
            if (h->dtor) {
                ti->hooks.move_dtor = flecs_default_dtor;
            }
        }
    }

error:
    return;
}

const ecs_type_hooks_t* ecs_get_hooks_id(
    ecs_world_t *world,
    ecs_entity_t id)
{
    const ecs_type_info_t *ti = ecs_get_type_info(world, id);
    if (ti) {
        return &ti->hooks;
    }
    return NULL;
}

void ecs_atfini(
    ecs_world_t *world,
    ecs_fini_action_t action,
    void *ctx)
{
    ecs_poly_assert(world, ecs_world_t);
    ecs_check(action != NULL, ECS_INVALID_PARAMETER, NULL);

    ecs_action_elem_t *elem = ecs_vec_append_t(NULL, &world->fini_actions,
        ecs_action_elem_t);
    ecs_assert(elem != NULL, ECS_INTERNAL_ERROR, NULL);

    elem->action = action;
    elem->ctx = ctx;
error:
    return;
}

void ecs_run_post_frame(
    ecs_world_t *world,
    ecs_fini_action_t action,
    void *ctx)
{
    ecs_check(world != NULL, ECS_INVALID_PARAMETER, NULL);
    ecs_check(action != NULL, ECS_INVALID_PARAMETER, NULL);
    
    ecs_stage_t *stage = flecs_stage_from_world(&world);
    ecs_action_elem_t *elem = ecs_vec_append_t(&stage->allocator, 
        &stage->post_frame_actions, ecs_action_elem_t);
    ecs_assert(elem != NULL, ECS_INTERNAL_ERROR, NULL);

    elem->action = action;
    elem->ctx = ctx; 
error:
    return;
}

/* Unset data in tables */
static
void flecs_fini_unset_tables(
    ecs_world_t *world)
{
    ecs_sparse_t *tables = &world->store.tables;
    int32_t i, count = flecs_sparse_count(tables);

    for (i = 0; i < count; i ++) {
        ecs_table_t *table = flecs_sparse_get_dense_t(tables, ecs_table_t, i);
        flecs_table_remove_actions(world, table);
    }
}

/* Invoke fini actions */
static
void flecs_fini_actions(
    ecs_world_t *world)
{
    int32_t i, count = ecs_vec_count(&world->fini_actions);
    ecs_action_elem_t *elems = ecs_vec_first(&world->fini_actions);
    for (i = 0; i < count; i ++) {
        elems[i].action(world, elems[i].ctx);
    }

    ecs_vec_fini_t(NULL, &world->fini_actions, ecs_action_elem_t);
}

/* Cleanup remaining type info elements */
static
void flecs_fini_type_info(
    ecs_world_t *world)
{
    int32_t i, count = flecs_sparse_count(&world->type_info);
    ecs_sparse_t *type_info = &world->type_info;
    for (i = 0; i < count; i ++) {
        ecs_type_info_t *ti = flecs_sparse_get_dense_t(type_info, 
            ecs_type_info_t, i);
        flecs_type_info_fini(ti);
    }
    flecs_sparse_fini(&world->type_info);
}

ecs_entity_t flecs_get_oneof(
    const ecs_world_t *world,
    ecs_entity_t e)
{
    if (ecs_is_alive(world, e)) {
        if (ecs_has_id(world, e, EcsOneOf)) {
            return e;
        } else {
            return ecs_get_target(world, e, EcsOneOf, 0);
        }
    } else {
        return 0;
    }
}

/* The destroyer of worlds */
int ecs_fini(
    ecs_world_t *world)
{
    ecs_poly_assert(world, ecs_world_t);
    ecs_assert(!(world->flags & EcsWorldReadonly), ECS_INVALID_OPERATION, NULL);
    ecs_assert(!(world->flags & EcsWorldFini), ECS_INVALID_OPERATION, NULL);
    ecs_assert(world->stages[0].defer == 0, ECS_INVALID_OPERATION, 
        "call defer_end before destroying world");

    ecs_trace("#[bold]shutting down world");
    ecs_log_push();

    world->flags |= EcsWorldQuit;

    /* Delete root entities first using regular APIs. This ensures that cleanup
     * policies get a chance to execute. */
    ecs_dbg_1("#[bold]cleanup root entities");
    ecs_log_push_1();
    flecs_fini_roots(world);
    ecs_log_pop_1();

    world->flags |= EcsWorldFini;

    /* Run fini actions (simple callbacks ran when world is deleted) before
     * destroying the storage */
    ecs_dbg_1("#[bold]run fini actions");
    ecs_log_push_1();
    flecs_fini_actions(world);
    ecs_log_pop_1();

    ecs_dbg_1("#[bold]cleanup remaining entities");
    ecs_log_push_1();

    /* Operations invoked during UnSet/OnRemove/destructors are deferred and
     * will be discarded after world cleanup */
    flecs_defer_begin(world, &world->stages[0]);

    /* Run UnSet/OnRemove actions for components while the store is still
     * unmodified by cleanup. */
    flecs_fini_unset_tables(world);

    /* This will destroy all entities and components. After this point no more
     * user code is executed. */
    flecs_fini_store(world);

    /* Purge deferred operations from the queue. This discards operations but
     * makes sure that any resources in the queue are freed */
    flecs_defer_purge(world, &world->stages[0]);
    ecs_log_pop_1();

    /* All queries are cleaned up, so monitors should've been cleaned up too */
    ecs_assert(!ecs_map_is_init(&world->monitors.monitors), 
        ECS_INTERNAL_ERROR, NULL);

    ecs_dbg_1("#[bold]cleanup world datastructures");
    ecs_log_push_1();
    flecs_entities_fini(world);
    flecs_sparse_fini(world->pending_tables);
    flecs_sparse_fini(world->pending_buffer);
    ecs_os_free(world->pending_tables);
    ecs_os_free(world->pending_buffer);
    flecs_fini_id_records(world);
    flecs_fini_type_info(world);
    flecs_observable_fini(&world->observable);
    flecs_name_index_fini(&world->aliases);
    flecs_name_index_fini(&world->symbols);
    ecs_set_stage_count(world, 0);
    ecs_log_pop_1();

    flecs_world_allocators_fini(world);

    /* End of the world */
    ecs_poly_free(world, ecs_world_t);
    ecs_os_fini();

    ecs_trace("world destroyed, bye!");
    ecs_log_pop();

    return 0;
}

bool ecs_is_fini(
    const ecs_world_t *world)
{
    ecs_assert(world != NULL, ECS_INVALID_PARAMETER, NULL);
    world = ecs_get_world(world);
    return ECS_BIT_IS_SET(world->flags, EcsWorldFini);
}

void ecs_dim(
    ecs_world_t *world,
    int32_t entity_count)
{
    ecs_poly_assert(world, ecs_world_t);
    flecs_entities_set_size(world, entity_count + FLECS_HI_COMPONENT_ID);
}

void flecs_eval_component_monitors(
    ecs_world_t *world)
{
    ecs_poly_assert(world, ecs_world_t);  
    flecs_process_pending_tables(world);  
    flecs_eval_component_monitor(world);
}

void ecs_measure_frame_time(
    ecs_world_t *world,
    bool enable)
{
    ecs_poly_assert(world, ecs_world_t);
    ecs_check(ecs_os_has_time(), ECS_MISSING_OS_API, NULL);

    if (world->info.target_fps == (ecs_ftime_t)0 || enable) {
        ECS_BIT_COND(world->flags, EcsWorldMeasureFrameTime, enable);
    }
error:
    return;
}

void ecs_measure_system_time(
    ecs_world_t *world,
    bool enable)
{
    ecs_poly_assert(world, ecs_world_t);
    ecs_check(ecs_os_has_time(), ECS_MISSING_OS_API, NULL);
    ECS_BIT_COND(world->flags, EcsWorldMeasureSystemTime, enable);
error:
    return;
}

void ecs_set_target_fps(
    ecs_world_t *world,
    ecs_ftime_t fps)
{
    ecs_poly_assert(world, ecs_world_t);
    ecs_check(ecs_os_has_time(), ECS_MISSING_OS_API, NULL);

    ecs_measure_frame_time(world, true);
    world->info.target_fps = fps;
error:
    return;
}

void* ecs_get_context(
    const ecs_world_t *world)
{
    ecs_check(world != NULL, ECS_INVALID_PARAMETER, NULL);    
    world = ecs_get_world(world);
    return world->context;
error:
    return NULL;
}

void ecs_set_context(
    ecs_world_t *world,
    void *context)
{
    ecs_poly_assert(world, ecs_world_t);
    world->context = context;
}

void ecs_set_entity_range(
    ecs_world_t *world,
    ecs_entity_t id_start,
    ecs_entity_t id_end)
{
    ecs_poly_assert(world, ecs_world_t);
    ecs_check(!id_end || id_end > id_start, ECS_INVALID_PARAMETER, NULL);
    ecs_check(!id_end || id_end > flecs_entities_max_id(world), 
        ECS_INVALID_PARAMETER, NULL);

    uint32_t start = (uint32_t)id_start;
    uint32_t end = (uint32_t)id_end;

    if (flecs_entities_max_id(world) < start) {
        flecs_entities_max_id(world) = start - 1;
    }

    world->info.min_id = start;
    world->info.max_id = end;
error:
    return;
}

bool ecs_enable_range_check(
    ecs_world_t *world,
    bool enable)
{
    ecs_poly_assert(world, ecs_world_t);    
    bool old_value = world->range_check_enabled;
    world->range_check_enabled = enable;
    return old_value;
}

ecs_entity_t ecs_get_max_id(
    const ecs_world_t *world)
{
    ecs_check(world != NULL, ECS_INVALID_PARAMETER, NULL);
    world = ecs_get_world(world);
    return flecs_entities_max_id(world);
error:
    return 0;
}

void ecs_set_entity_generation(
    ecs_world_t *world,
    ecs_entity_t entity_with_generation)
{
    ecs_poly_assert(world, ecs_world_t);
    ecs_assert(!(world->flags & EcsWorldReadonly), ECS_INVALID_OPERATION, NULL);
    ecs_assert(!(ecs_is_deferred(world)), ECS_INVALID_OPERATION, NULL);

    flecs_entities_set_generation(world, entity_with_generation);

    ecs_record_t *r = flecs_entities_get(world, entity_with_generation);
    if (r && r->table) {
        int32_t row = ECS_RECORD_TO_ROW(r->row);
        ecs_entity_t *entities = r->table->data.entities.array;
        entities[row] = entity_with_generation;
    }
}

const ecs_type_info_t* flecs_type_info_get(
    const ecs_world_t *world,
    ecs_entity_t component)
{
    ecs_poly_assert(world, ecs_world_t);   

    ecs_assert(component != 0, ECS_INTERNAL_ERROR, NULL);
    ecs_assert(!(component & ECS_ID_FLAGS_MASK), ECS_INTERNAL_ERROR, NULL);

    return flecs_sparse_try_t(&world->type_info, ecs_type_info_t, component);
}

ecs_type_info_t* flecs_type_info_ensure(
    ecs_world_t *world,
    ecs_entity_t component)
{
    ecs_poly_assert(world, ecs_world_t);
    ecs_assert(component != 0, ECS_INTERNAL_ERROR, NULL);

    const ecs_type_info_t *ti = flecs_type_info_get(world, component);
    ecs_type_info_t *ti_mut = NULL;
    if (!ti) {
        ti_mut = flecs_sparse_ensure_t(
            &world->type_info, ecs_type_info_t, component);
        ecs_assert(ti_mut != NULL, ECS_INTERNAL_ERROR, NULL);
        ti_mut->component = component;
    } else {
        ti_mut = (ecs_type_info_t*)ti;
    }

    if (!ti_mut->name) {
        const char *sym = ecs_get_symbol(world, component);
        if (sym) {
            ti_mut->name = ecs_os_strdup(sym);
        } else {
            const char *name = ecs_get_name(world, component);
            if (name) {
                ti_mut->name = ecs_os_strdup(name);
            }
        }
    }

    return ti_mut;
}

bool flecs_type_info_init_id(
    ecs_world_t *world,
    ecs_entity_t component,
    ecs_size_t size,
    ecs_size_t alignment,
    const ecs_type_hooks_t *li)
{
    bool changed = false;

    flecs_entities_ensure(world, component);

    ecs_type_info_t *ti = NULL;
    if (!size || !alignment) {
        ecs_assert(size == 0 && alignment == 0, 
            ECS_INVALID_COMPONENT_SIZE, NULL);
        ecs_assert(li == NULL, ECS_INCONSISTENT_COMPONENT_ACTION, NULL);
        flecs_sparse_remove_t(&world->type_info, ecs_type_info_t, component);
    } else {
        ti = flecs_type_info_ensure(world, component);
        ecs_assert(ti != NULL, ECS_INTERNAL_ERROR, NULL);
        changed |= ti->size != size;
        changed |= ti->alignment != alignment;
        ti->size = size;
        ti->alignment = alignment;
        if (li) {
            ecs_set_hooks_id(world, component, li);
        }
    }

    /* Set type info for id record of component */
    ecs_id_record_t *idr = flecs_id_record_ensure(world, component);
    changed |= flecs_id_record_set_type_info(world, idr, ti);
    bool is_tag = idr->flags & EcsIdTag;

    /* All id records with component as relationship inherit type info */
    idr = flecs_id_record_ensure(world, ecs_pair(component, EcsWildcard));
    do {
        if (is_tag) {
            changed |= flecs_id_record_set_type_info(world, idr, NULL);
        } else if (ti) {
            changed |= flecs_id_record_set_type_info(world, idr, ti);
        } else if ((idr->type_info != NULL) && 
            (idr->type_info->component == component))
        {
            changed |= flecs_id_record_set_type_info(world, idr, NULL);
        }
    } while ((idr = idr->first.next));

    /* All non-tag id records with component as object inherit type info,
     * if relationship doesn't have type info */
    idr = flecs_id_record_ensure(world, ecs_pair(EcsWildcard, component));
    do {
        if (!(idr->flags & EcsIdTag) && !idr->type_info) {
            changed |= flecs_id_record_set_type_info(world, idr, ti);
        }
    } while ((idr = idr->first.next));

    /* Type info of (*, component) should always point to component */
    ecs_assert(flecs_id_record_get(world, ecs_pair(EcsWildcard, component))->
        type_info == ti, ECS_INTERNAL_ERROR, NULL);

    return changed;
}

void flecs_type_info_fini(
    ecs_type_info_t *ti)
{
    if (ti->hooks.ctx_free) {
        ti->hooks.ctx_free(ti->hooks.ctx);
    }
    if (ti->hooks.binding_ctx_free) {
        ti->hooks.binding_ctx_free(ti->hooks.binding_ctx);
    }
    if (ti->name) {
        /* Safe to cast away const, world has ownership over string */
        ecs_os_free((char*)ti->name);
        ti->name = NULL;
    }
}

void flecs_type_info_free(
    ecs_world_t *world,
    ecs_entity_t component)
{
    if (world->flags & EcsWorldQuit) {
        /* If world is in the final teardown stages, cleanup policies are no
         * longer applied and it can't be guaranteed that a component is not
         * deleted before entities that use it. The remaining type info elements
         * will be deleted after the store is finalized. */
        return;
    }

    ecs_type_info_t *ti = flecs_sparse_try_t(&world->type_info, 
        ecs_type_info_t, component);
    if (ti) {
        flecs_type_info_fini(ti);
        flecs_sparse_remove_t(&world->type_info, ecs_type_info_t, component);
    }
}

static
ecs_ftime_t flecs_insert_sleep(
    ecs_world_t *world,
    ecs_time_t *stop)
{
    ecs_poly_assert(world, ecs_world_t);  

    ecs_time_t start = *stop, now = start;
    ecs_ftime_t delta_time = (ecs_ftime_t)ecs_time_measure(stop);

    if (world->info.target_fps == (ecs_ftime_t)0.0) {
        return delta_time;
    }

    ecs_ftime_t target_delta_time = 
        ((ecs_ftime_t)1.0 / (ecs_ftime_t)world->info.target_fps);

    /* Calculate the time we need to sleep by taking the measured delta from the
     * previous frame, and subtracting it from target_delta_time. */
    ecs_ftime_t sleep = target_delta_time - delta_time;

    /* Pick a sleep interval that is 4 times smaller than the time one frame
     * should take. */
    ecs_ftime_t sleep_time = sleep / (ecs_ftime_t)4.0;

    do {
        /* Only call sleep when sleep_time is not 0. On some platforms, even
         * a sleep with a timeout of 0 can cause stutter. */
        if (sleep_time != 0) {
            ecs_sleepf((double)sleep_time);
        }

        now = start;
        delta_time = (ecs_ftime_t)ecs_time_measure(&now);
    } while ((target_delta_time - delta_time) > 
        (sleep_time / (ecs_ftime_t)2.0));

    *stop = now;
    return delta_time;
}

static
ecs_ftime_t flecs_start_measure_frame(
    ecs_world_t *world,
    ecs_ftime_t user_delta_time)
{
    ecs_poly_assert(world, ecs_world_t);  

    ecs_ftime_t delta_time = 0;

    if ((world->flags & EcsWorldMeasureFrameTime) || (user_delta_time == 0)) {
        ecs_time_t t = world->frame_start_time;
        do {
            if (world->frame_start_time.nanosec || world->frame_start_time.sec){ 
                delta_time = flecs_insert_sleep(world, &t);
            } else {
                ecs_time_measure(&t);
                if (world->info.target_fps != 0) {
                    delta_time = (ecs_ftime_t)1.0 / world->info.target_fps;
                } else {
                    /* Best guess */
                    delta_time = (ecs_ftime_t)1.0 / (ecs_ftime_t)60.0; 
                }
            }
        
        /* Keep trying while delta_time is zero */
        } while (delta_time == 0);

        world->frame_start_time = t;  

        /* Keep track of total time passed in world */
        world->info.world_time_total_raw += (ecs_ftime_t)delta_time;
    }

    return (ecs_ftime_t)delta_time;
}

static
void flecs_stop_measure_frame(
    ecs_world_t* world)
{
    ecs_poly_assert(world, ecs_world_t);  

    if (world->flags & EcsWorldMeasureFrameTime) {
        ecs_time_t t = world->frame_start_time;
        world->info.frame_time_total += (ecs_ftime_t)ecs_time_measure(&t);
    }
}

ecs_ftime_t ecs_frame_begin(
    ecs_world_t *world,
    ecs_ftime_t user_delta_time)
{
    ecs_poly_assert(world, ecs_world_t);
    ecs_check(!(world->flags & EcsWorldReadonly), ECS_INVALID_OPERATION, NULL);
    ecs_check(user_delta_time != 0 || ecs_os_has_time(), 
        ECS_MISSING_OS_API, "get_time");

    /* Start measuring total frame time */
    ecs_ftime_t delta_time = flecs_start_measure_frame(world, user_delta_time);
    if (user_delta_time == 0) {
        user_delta_time = delta_time;
    }  

    world->info.delta_time_raw = user_delta_time;
    world->info.delta_time = user_delta_time * world->info.time_scale;

    /* Keep track of total scaled time passed in world */
    world->info.world_time_total += world->info.delta_time;

    ecs_run_aperiodic(world, 0);

    return world->info.delta_time;
error:
    return (ecs_ftime_t)0;
}

void ecs_frame_end(
    ecs_world_t *world)
{
    ecs_poly_assert(world, ecs_world_t);
    ecs_check(!(world->flags & EcsWorldReadonly), ECS_INVALID_OPERATION, NULL);

    world->info.frame_count_total ++;
    
    ecs_stage_t *stages = world->stages;
    int32_t i, count = world->stage_count;
    for (i = 0; i < count; i ++) {
        flecs_stage_merge_post_frame(world, &stages[i]);
    }

    flecs_stop_measure_frame(world);
error:
    return;
}

const ecs_world_info_t* ecs_get_world_info(
    const ecs_world_t *world)
{
    world = ecs_get_world(world);
    return &world->info;
}

void flecs_delete_table(
    ecs_world_t *world,
    ecs_table_t *table)
{
    ecs_poly_assert(world, ecs_world_t); 
    flecs_table_release(world, table);
}

static
void flecs_process_empty_queries(
    ecs_world_t *world)
{
    ecs_poly_assert(world, ecs_world_t); 

    ecs_id_record_t *idr = flecs_id_record_get(world, 
        ecs_pair(ecs_id(EcsPoly), EcsQuery));
    if (!idr) {
        return;
    }

    ecs_run_aperiodic(world, EcsAperiodicEmptyTables);

    /* Make sure that we defer adding the inactive tags until after iterating
     * the query */
    flecs_defer_begin(world, &world->stages[0]);

    ecs_table_cache_iter_t it;
    const ecs_table_record_t *tr;
    if (flecs_table_cache_iter(&idr->cache, &it)) {
        while ((tr = flecs_table_cache_next(&it, ecs_table_record_t))) {
            ecs_table_t *table = tr->hdr.table;
            EcsPoly *queries = ecs_table_get_column(table, tr->column, 0);
            int32_t i, count = ecs_table_count(table);

            for (i = 0; i < count; i ++) {
                ecs_query_t *query = queries[i].poly;
                ecs_entity_t *entities = table->data.entities.array;
                if (!ecs_query_table_count(query)) {
                    ecs_add_id(world, entities[i], EcsEmpty);
                }
            }
        }
    }

    flecs_defer_end(world, &world->stages[0]);
}

/** Walk over tables that had a state change which requires bookkeeping */
void flecs_process_pending_tables(
    const ecs_world_t *world_r)
{
    ecs_poly_assert(world_r, ecs_world_t);

    /* We can't update the administration while in readonly mode, but we can
     * ensure that when this function is called there are no pending events. */
    if (world_r->flags & EcsWorldReadonly) {
        ecs_assert(flecs_sparse_count(world_r->pending_tables) == 0,
            ECS_INTERNAL_ERROR, NULL);
        return;
    }

    /* Safe to cast, world is not readonly */
    ecs_world_t *world = (ecs_world_t*)world_r;
    
    /* If pending buffer is NULL there already is a stackframe that's iterating
     * the table list. This can happen when an observer for a table event results
     * in a mutation that causes another table to change state. A typical 
     * example of this is a system that becomes active/inactive as the result of
     * a query (and as a result, its matched tables) becoming empty/non empty */
    if (!world->pending_buffer) {
        return;
    }

    /* Swap buffer. The logic could in theory have been implemented with a
     * single sparse set, but that would've complicated (and slowed down) the
     * iteration. Additionally, by using a double buffer approach we can still
     * keep most of the original ordering of events intact, which is desirable
     * as it means that the ordering of tables in the internal datastructures is
     * more predictable. */
    int32_t i, count = flecs_sparse_count(world->pending_tables);
    if (!count) {
        return;
    }

    flecs_journal_begin(world, EcsJournalTableEvents, 0, 0, 0);

    do {
        ecs_sparse_t *pending_tables = world->pending_tables;
        world->pending_tables = world->pending_buffer;
        world->pending_buffer = NULL;

        /* Make sure that any ECS operations that occur while delivering the
         * events does not cause inconsistencies, like sending an Empty 
         * notification for a table that just became non-empty. */
        flecs_defer_begin(world, &world->stages[0]);

        for (i = 0; i < count; i ++) {
            ecs_table_t *table = flecs_sparse_get_dense_t(
                pending_tables, ecs_table_t*, i)[0];
            if (!table->id) {
                /* Table is being deleted, ignore empty events */
                continue;
            }

            /* For each id in the table, add it to the empty/non empty list
             * based on its current state */
            if (flecs_table_records_update_empty(table)) {
                int32_t table_count = ecs_table_count(table);
                if (table->flags & (EcsTableHasOnTableFill|EcsTableHasOnTableEmpty)) {
                    /* Only emit an event when there was a change in the 
                    * administration. It is possible that a table ended up in the
                    * pending_tables list by going from empty->non-empty, but then
                    * became empty again. By the time we run this code, no changes
                    * in the administration would actually be made. */
                    ecs_entity_t evt = table_count ? EcsOnTableFill : EcsOnTableEmpty;
                    if (ecs_should_log_3()) {
                        ecs_dbg_3("table %u state change (%s)", 
                            (uint32_t)table->id,
                            table_count ? "non-empty" : "empty");
                    }

                    ecs_log_push_3();

                    flecs_emit(world, world, &(ecs_event_desc_t){
                        .event = evt,
                        .table = table,
                        .ids = &table->type,
                        .observable = world,
                        .flags = EcsEventTableOnly
                    });

                    ecs_log_pop_3();   
                }
                world->info.empty_table_count += (table_count == 0) * 2 - 1;
            }
        }

        flecs_sparse_clear(pending_tables);
        ecs_defer_end(world);

        world->pending_buffer = pending_tables;
    } while ((count = flecs_sparse_count(world->pending_tables)));

    flecs_journal_end();
}

void flecs_table_set_empty(
    ecs_world_t *world,
    ecs_table_t *table)
{
    ecs_poly_assert(world, ecs_world_t);
    ecs_assert(!(world->flags & EcsWorldReadonly), ECS_INTERNAL_ERROR, NULL);
    ecs_assert(table != NULL, ECS_INTERNAL_ERROR, NULL);

    if (ecs_table_count(table)) {
        table->_->generation = 0;
    }

    flecs_sparse_ensure_fast_t(world->pending_tables, ecs_table_t*, 
        (uint32_t)table->id)[0] = table;
}

bool ecs_id_in_use(
    const ecs_world_t *world,
    ecs_id_t id)
{
    ecs_id_record_t *idr = flecs_id_record_get(world, id);
    if (!idr) {
        return false;
    }
    return (flecs_table_cache_count(&idr->cache) != 0) ||
        (flecs_table_cache_empty_count(&idr->cache) != 0);
}

void ecs_run_aperiodic(
    ecs_world_t *world,
    ecs_flags32_t flags)
{
    ecs_poly_assert(world, ecs_world_t);
    
    if (!flags || (flags & EcsAperiodicEmptyTables)) {
        flecs_process_pending_tables(world);
    }
    if ((flags & EcsAperiodicEmptyQueries)) {
        flecs_process_empty_queries(world);
    }
    if (!flags || (flags & EcsAperiodicComponentMonitors)) {
        flecs_eval_component_monitors(world);
    }
}

int32_t ecs_delete_empty_tables(
    ecs_world_t *world,
    ecs_id_t id,
    uint16_t clear_generation,
    uint16_t delete_generation,
    int32_t min_id_count,
    double time_budget_seconds)
{
    ecs_poly_assert(world, ecs_world_t);

    /* Make sure empty tables are in the empty table lists */
    ecs_run_aperiodic(world, EcsAperiodicEmptyTables);

    ecs_time_t start = {0}, cur = {0};
    int32_t delete_count = 0, clear_count = 0;
    bool time_budget = false;

    if (time_budget_seconds != 0 || (ecs_should_log_1() && ecs_os_has_time())) {
        ecs_time_measure(&start);
    }

    if (time_budget_seconds != 0) {
        time_budget = true;
    }

    if (!id) {
        id = EcsAny; /* Iterate all empty tables */
    }

    ecs_id_record_t *idr = flecs_id_record_get(world, id);
    ecs_table_cache_iter_t it;
    if (idr && flecs_table_cache_empty_iter((ecs_table_cache_t*)idr, &it)) {
        ecs_table_record_t *tr;
        while ((tr = flecs_table_cache_next(&it, ecs_table_record_t))) {
            if (time_budget) {
                cur = start;
                if (ecs_time_measure(&cur) > time_budget_seconds) {
                    goto done;
                }
            }

            ecs_table_t *table = tr->hdr.table;
            ecs_assert(ecs_table_count(table) == 0, ECS_INTERNAL_ERROR, NULL);
            if (table->_->refcount > 1) {
                /* Don't delete claimed tables */
                continue;
            }

            if (table->type.count < min_id_count) {
                continue;
            }

            uint16_t gen = ++ table->_->generation;
            if (delete_generation && (gen > delete_generation)) {
                if (flecs_table_release(world, table)) {
                    delete_count ++;
                }
            } else if (clear_generation && (gen > clear_generation)) {
                if (flecs_table_shrink(world, table)) {
                    clear_count ++;
                }
            }
        }
    }

done:
    if (ecs_should_log_1() && ecs_os_has_time()) {
        if (delete_count) {
            ecs_dbg_1("#[red]deleted#[normal] %d empty tables in %.2fs", 
                delete_count, ecs_time_measure(&start));
        }
        if (clear_count) {
            ecs_dbg_1("#[red]cleared#[normal] %d empty tables in %.2fs", 
                clear_count, ecs_time_measure(&start));
        }
    }

    return delete_count;
}

/**
 * @file observable.c
 * @brief Observable implementation.
 * 
 * The observable implementation contains functions that find the set of 
 * observers to invoke for an event. The code also contains the implementation
 * of a reachable id cache, which is used to speedup event propagation when
 * relationships are added/removed to/from entities.
 */


void flecs_observable_init(
    ecs_observable_t *observable)
{
    flecs_sparse_init_t(&observable->events, NULL, NULL, ecs_event_record_t);
    observable->on_add.event = EcsOnAdd;
    observable->on_remove.event = EcsOnRemove;
    observable->on_set.event = EcsOnSet;
    observable->un_set.event = EcsUnSet;
}

void flecs_observable_fini(
    ecs_observable_t *observable)
{
    ecs_assert(!ecs_map_is_init(&observable->on_add.event_ids), 
        ECS_INTERNAL_ERROR, NULL);
    ecs_assert(!ecs_map_is_init(&observable->on_remove.event_ids), 
        ECS_INTERNAL_ERROR, NULL);
    ecs_assert(!ecs_map_is_init(&observable->on_set.event_ids), 
        ECS_INTERNAL_ERROR, NULL);
    ecs_assert(!ecs_map_is_init(&observable->un_set.event_ids), 
        ECS_INTERNAL_ERROR, NULL);

    ecs_sparse_t *events = &observable->events;
    int32_t i, count = flecs_sparse_count(events);
    for (i = 0; i < count; i ++) {
        ecs_event_record_t *er = 
            flecs_sparse_get_dense_t(events, ecs_event_record_t, i);
        ecs_assert(er != NULL, ECS_INTERNAL_ERROR, NULL);
        (void)er;

        /* All triggers should've unregistered by now */
        ecs_assert(!ecs_map_is_init(&er->event_ids), 
            ECS_INTERNAL_ERROR, NULL);
    }

    flecs_sparse_fini(&observable->events);
}

ecs_event_record_t* flecs_event_record_get(
    const ecs_observable_t *o,
    ecs_entity_t event)
{
    ecs_assert(o != NULL, ECS_INTERNAL_ERROR, NULL);
    
    /* Builtin events*/
    if      (event == EcsOnAdd)     return (ecs_event_record_t*)&o->on_add;
    else if (event == EcsOnRemove)  return (ecs_event_record_t*)&o->on_remove;
    else if (event == EcsOnSet)     return (ecs_event_record_t*)&o->on_set;
    else if (event == EcsUnSet)     return (ecs_event_record_t*)&o->un_set;
    else if (event == EcsWildcard)  return (ecs_event_record_t*)&o->on_wildcard;

    /* User events */
    return flecs_sparse_try_t(&o->events, ecs_event_record_t, event);
}

ecs_event_record_t* flecs_event_record_ensure(
    ecs_observable_t *o,
    ecs_entity_t event)
{
    ecs_assert(o != NULL, ECS_INTERNAL_ERROR, NULL);

    ecs_event_record_t *er = flecs_event_record_get(o, event);
    if (er) {
        return er;
    }
    er = flecs_sparse_ensure_t(&o->events, ecs_event_record_t, event);
    er->event = event;
    return er;
}

static
ecs_event_record_t* flecs_event_record_get_if(
    const ecs_observable_t *o,
    ecs_entity_t event)
{
    ecs_assert(o != NULL, ECS_INTERNAL_ERROR, NULL);

    ecs_event_record_t *er = flecs_event_record_get(o, event);
    if (er) {
        if (ecs_map_is_init(&er->event_ids)) {
            return er;
        }
        if (er->any) {
            return er;
        }
        if (er->wildcard) {
            return er;
        }
        if (er->wildcard_pair) {
            return er;
        }
    }

    return NULL;
}

ecs_event_id_record_t* flecs_event_id_record_get(
    const ecs_event_record_t *er,
    ecs_id_t id)
{
    if (!er) {
        return NULL;
    }

    if (id == EcsAny)                                  return er->any;
    else if (id == EcsWildcard)                        return er->wildcard;
    else if (id == ecs_pair(EcsWildcard, EcsWildcard)) return er->wildcard_pair;
    else {
        if (ecs_map_is_init(&er->event_ids)) {
            return ecs_map_get_deref(&er->event_ids, ecs_event_id_record_t, id);
        }
        return NULL;
    }
}

static
ecs_event_id_record_t* flecs_event_id_record_get_if(
    const ecs_event_record_t *er,
    ecs_id_t id)
{
    ecs_event_id_record_t *ider = flecs_event_id_record_get(er, id);
    if (!ider) {
        return NULL;
    }

    if (ider->observer_count) {
        return ider;
    }

    return NULL;
}

ecs_event_id_record_t* flecs_event_id_record_ensure(
    ecs_world_t *world,
    ecs_event_record_t *er,
    ecs_id_t id)
{
    ecs_event_id_record_t *ider = flecs_event_id_record_get(er, id);
    if (ider) {
        return ider;
    }

    ider = ecs_os_calloc_t(ecs_event_id_record_t);

    if (id == EcsAny) {
        return er->any = ider;
    } else if (id == EcsWildcard) {
        return er->wildcard = ider;
    } else if (id == ecs_pair(EcsWildcard, EcsWildcard)) {
        return er->wildcard_pair = ider;
    }

    ecs_map_init_w_params_if(&er->event_ids, &world->allocators.ptr);
    ecs_map_insert_ptr(&er->event_ids, id, ider);
    return ider;
}

void flecs_event_id_record_remove(
    ecs_event_record_t *er,
    ecs_id_t id)
{
    if (id == EcsAny) {
        er->any = NULL;
    } else if (id == EcsWildcard) {
        er->wildcard = NULL;
    } else if (id == ecs_pair(EcsWildcard, EcsWildcard)) {
        er->wildcard_pair = NULL;
    } else {
        ecs_map_remove(&er->event_ids, id);
        if (!ecs_map_count(&er->event_ids)) {
            ecs_map_fini(&er->event_ids);
        }
    }
}

static
int32_t flecs_event_observers_get(
    const ecs_event_record_t *er,
    ecs_id_t id,
    ecs_event_id_record_t **iders)
{
    if (!er) {
        return 0;
    }

    /* Populate array with observer sets matching the id */
    int32_t count = 0;
    iders[0] = flecs_event_id_record_get_if(er, EcsAny);
    count += iders[count] != 0;

    iders[count] = flecs_event_id_record_get_if(er, id);
    count += iders[count] != 0;

    if (ECS_IS_PAIR(id)) {
        ecs_id_t id_fwc = ecs_pair(EcsWildcard, ECS_PAIR_SECOND(id));
        ecs_id_t id_swc = ecs_pair(ECS_PAIR_FIRST(id), EcsWildcard);
        ecs_id_t id_pwc = ecs_pair(EcsWildcard, EcsWildcard);
        iders[count] = flecs_event_id_record_get_if(er, id_fwc);
        count += iders[count] != 0;
        iders[count] = flecs_event_id_record_get_if(er, id_swc);
        count += iders[count] != 0;
        iders[count] = flecs_event_id_record_get_if(er, id_pwc);
        count += iders[count] != 0;
    } else {
        iders[count] = flecs_event_id_record_get_if(er, EcsWildcard);
        count += iders[count] != 0;
    }

    return count;
}

bool flecs_observers_exist(
    ecs_observable_t *observable,
    ecs_id_t id,
    ecs_entity_t event)
{
    ecs_event_record_t *er = flecs_event_record_get_if(observable, event);
    if (!er) {
        return false;
    }

    return flecs_event_id_record_get_if(er, id) != NULL;
}

static
void flecs_emit_propagate(
    ecs_world_t *world,
    ecs_iter_t *it,
    ecs_id_record_t *idr,
    ecs_id_record_t *tgt_idr,
    ecs_event_id_record_t **iders,
    int32_t ider_count)
{
    ecs_assert(tgt_idr != NULL, ECS_INTERNAL_ERROR, NULL);    

    if (ecs_should_log_3()) {
        char *idstr = ecs_id_str(world, tgt_idr->id);
        ecs_dbg_3("propagate events/invalidate cache for %s", idstr);
        ecs_os_free(idstr);
    }
    ecs_log_push_3();

    /* Propagate to records of traversable relationships */
    ecs_id_record_t *cur = tgt_idr;
    while ((cur = cur->trav.next)) {
        cur->reachable.generation ++; /* Invalidate cache */

        ecs_table_cache_iter_t idt;
        if (!flecs_table_cache_all_iter(&cur->cache, &idt)) {
            continue;
        }

        /* Get traversed relationship */
        ecs_entity_t trav = ECS_PAIR_FIRST(cur->id);

        const ecs_table_record_t *tr;
        while ((tr = flecs_table_cache_next(&idt, ecs_table_record_t))) {
            ecs_table_t *table = tr->hdr.table;
            if (!ecs_table_count(table)) {
                continue;
            }

            bool owned = flecs_id_record_get_table(idr, table);

            int32_t e, entity_count = ecs_table_count(table);
            it->table = table;
            it->other_table = NULL;
            it->offset = 0;
            it->count = entity_count;
            if (entity_count) {
                it->entities = ecs_vec_first(&table->data.entities);
            }

            /* Treat as new event as this could invoke observers again for
             * different tables. */
            int32_t evtx = ++ world->event_id;

            int32_t ider_i;
            for (ider_i = 0; ider_i < ider_count; ider_i ++) {
                ecs_event_id_record_t *ider = iders[ider_i];
                flecs_observers_invoke(world, &ider->up, it, table, trav, evtx);

                if (!owned) {
                    /* Owned takes precedence */
                    flecs_observers_invoke(
                        world, &ider->self_up, it, table, trav, evtx);
                }
            }

            if (!table->_->traversable_count) {
                continue;
            }

            ecs_record_t **records = ecs_vec_first(&table->data.records);
            for (e = 0; e < entity_count; e ++) {
                ecs_record_t *r = records[e];
                ecs_assert(r != NULL, ECS_INTERNAL_ERROR, NULL);
                ecs_id_record_t *idr_t = r->idr;
                if (idr_t) {
                    /* Only notify for entities that are used in pairs with
                     * traversable relationships */
                    flecs_emit_propagate(world, it, idr, idr_t,
                        iders, ider_count);
                }
            }
        }
    }

    ecs_log_pop_3();
}

static
void flecs_emit_propagate_invalidate_tables(
    ecs_world_t *world,
    ecs_id_record_t *tgt_idr)
{
    ecs_assert(tgt_idr != NULL, ECS_INTERNAL_ERROR, NULL);

    if (ecs_should_log_3()) {
        char *idstr = ecs_id_str(world, tgt_idr->id);
        ecs_dbg_3("invalidate reachable cache for %s", idstr);
        ecs_os_free(idstr);
    }

    /* Invalidate records of traversable relationships */
    ecs_id_record_t *cur = tgt_idr;
    while ((cur = cur->trav.next)) {
        ecs_reachable_cache_t *rc = &cur->reachable;
        if (rc->current != rc->generation) {
            /* Subtree is already marked invalid */
            continue;
        }

        rc->generation ++;

        ecs_table_cache_iter_t idt;
        if (!flecs_table_cache_all_iter(&cur->cache, &idt)) {
            continue;
        }

        const ecs_table_record_t *tr;
        while ((tr = flecs_table_cache_next(&idt, ecs_table_record_t))) {
            ecs_table_t *table = tr->hdr.table;
            if (!table->_->traversable_count) {
                continue;
            }

            int32_t e, entity_count = ecs_table_count(table);
            ecs_record_t **records = ecs_vec_first(&table->data.records);

            for (e = 0; e < entity_count; e ++) {
                ecs_id_record_t *idr_t = records[e]->idr;
                if (idr_t) {
                    /* Only notify for entities that are used in pairs with
                     * traversable relationships */
                    flecs_emit_propagate_invalidate_tables(world, idr_t);
                }
            }
        }
    }
}

void flecs_emit_propagate_invalidate(
    ecs_world_t *world,
    ecs_table_t *table,
    int32_t offset,
    int32_t count)
{
    ecs_record_t **recs = ecs_vec_get_t(&table->data.records, 
        ecs_record_t*, offset);
    int32_t i;
    for (i = 0; i < count; i ++) {
        ecs_record_t *record = recs[i];
        if (!record) {
            /* If the event is emitted after a bulk operation, it's possible
             * that it hasn't been populated with entities yet. */
            continue;
        }

        ecs_id_record_t *idr_t = record->idr;
        if (idr_t) {
            /* Event is used as target in traversable relationship, propagate */
            flecs_emit_propagate_invalidate_tables(world, idr_t);
        }
    }
}

static
void flecs_override_copy(
    ecs_world_t *world,
    ecs_table_t *table,
    const ecs_type_info_t *ti,
    void *dst,
    const void *src,
    int32_t offset,
    int32_t count)
{
    void *ptr = dst;
    ecs_copy_t copy = ti->hooks.copy;
    ecs_size_t size = ti->size;
    int32_t i;
    if (copy) {
        for (i = 0; i < count; i ++) {
            copy(ptr, src, count, ti);
            ptr = ECS_OFFSET(ptr, size);
        }
    } else {
        for (i = 0; i < count; i ++) {
            ecs_os_memcpy(ptr, src, size);
            ptr = ECS_OFFSET(ptr, size);
        }
    }

    ecs_iter_action_t on_set = ti->hooks.on_set;
    if (on_set) {
        ecs_entity_t *entities = ecs_vec_get_t(
            &table->data.entities, ecs_entity_t, offset);
        flecs_invoke_hook(world, table, count, offset, entities,
            dst, ti->component, ti, EcsOnSet, on_set);
    }
}

static
void* flecs_override(
    ecs_iter_t *it, 
    const ecs_type_t *emit_ids,
    ecs_id_t id,
    ecs_table_t *table,
    ecs_id_record_t *idr)
{
    if (it->event != EcsOnAdd || (it->flags & EcsEventNoOnSet)) {
        return NULL;
    }

    int32_t i = 0, count = emit_ids->count;
    ecs_id_t *ids = emit_ids->array;
    for (i = 0; i < count; i ++) {
        if (ids[i] == id) {
            /* If an id was both inherited and overridden in the same event
             * (like what happens during an auto override), we need to copy the
             * value of the inherited component to the new component.
             * Also flag to the callee that this component was overridden, so
             * that an OnSet event can be emmitted for it.
             * Note that this is different from a component that was overridden
             * after it was inherited, as this does not change the actual value
             * of the component for the entity (it is copied from the existing
             * overridden component), and does not require an OnSet event. */
            const ecs_table_record_t *tr = flecs_id_record_get_table(idr, table);
            if (!tr) {
                continue;
            }

            int32_t column = tr->column;
            column = ecs_table_type_to_storage_index(table, column);
            ecs_assert(column != -1, ECS_INTERNAL_ERROR, NULL);

            const ecs_type_info_t *ti = idr->type_info;
            ecs_assert(ti != NULL, ECS_INTERNAL_ERROR, NULL);
            ecs_size_t size = ti->size;
            ecs_vec_t *vec = &table->data.columns[column];
            return ecs_vec_get(vec, size, it->offset);
        }
    }

    return NULL;
}

static
void flecs_emit_forward_up(
    ecs_world_t *world,
    ecs_event_record_t *er,
    ecs_event_record_t *er_onset,
    const ecs_type_t *emit_ids,
    ecs_iter_t *it,
    ecs_table_t *table,
    ecs_id_record_t *idr,
    ecs_vec_t *stack,
    ecs_vec_t *reachable_ids,
    int32_t evtx);

static
void flecs_emit_forward_id(
    ecs_world_t *world,
    ecs_event_record_t *er,
    ecs_event_record_t *er_onset,
    const ecs_type_t *emit_ids,
    ecs_iter_t *it,
    ecs_table_t *table,
    ecs_id_record_t *idr,
    ecs_entity_t tgt,
    ecs_table_t *tgt_table,
    int32_t column,
    int32_t offset,
    ecs_entity_t trav,
    int32_t evtx)
{
    ecs_id_t id = idr->id;
    ecs_entity_t event = er ? er->event : 0;
    bool inherit = trav == EcsIsA;
    bool may_override = inherit && (event == EcsOnAdd) && (emit_ids->count > 1);
    ecs_event_id_record_t *iders[5];
    ecs_event_id_record_t *iders_onset[5];

    /* Skip id if there are no observers for it */
    int32_t ider_i, ider_count = flecs_event_observers_get(er, id, iders);
    int32_t ider_onset_i, ider_onset_count = 0;
    if (er_onset) {
        ider_onset_count = flecs_event_observers_get(
            er_onset, id, iders_onset);
    }

    if (!may_override && (!ider_count && !ider_onset_count)) {
        return;
    }

    it->ids[0] = id;
    it->sources[0] = tgt;
    it->event_id = id;
    it->ptrs[0] = NULL;
    it->sizes[0] = 0;

    int32_t storage_i = ecs_table_type_to_storage_index(tgt_table, column);
    if (storage_i != -1) {
        ecs_assert(idr->type_info != NULL, ECS_INTERNAL_ERROR, NULL);
        ecs_vec_t *vec = &tgt_table->data.columns[storage_i];
        ecs_size_t size = idr->type_info->size;
        it->ptrs[0] = ecs_vec_get(vec, size, offset);
        it->sizes[0] = size;
    }

    const ecs_table_record_t *tr = flecs_id_record_get_table(idr, table);
    bool owned = tr != NULL;

    for (ider_i = 0; ider_i < ider_count; ider_i ++) {
        ecs_event_id_record_t *ider = iders[ider_i];
        flecs_observers_invoke(world, &ider->up, it, table, trav, evtx);

        /* Owned takes precedence */
        if (!owned) {
            flecs_observers_invoke(world, &ider->self_up, it, table, trav, evtx);
        }
    }

    /* Emit OnSet events for newly inherited components */
    if (storage_i != -1) {
        bool override = false;

        /* If component was added together with IsA relationship, still emit
         * OnSet event, as it's a new value for the entity. */
        void *base_ptr = it->ptrs[0];
        void *ptr = flecs_override(it, emit_ids, id, table, idr);
        if (ptr) {
            override = true;
            it->ptrs[0] = ptr;
        }

        if (ider_onset_count) {
            it->event = er_onset->event;

            for (ider_onset_i = 0; ider_onset_i < ider_onset_count; ider_onset_i ++) {
                ecs_event_id_record_t *ider = iders_onset[ider_onset_i];
                flecs_observers_invoke(world, &ider->up, it, table, trav, evtx);

                /* Owned takes precedence */
                if (!owned) {
                    flecs_observers_invoke(
                        world, &ider->self_up, it, table, trav, evtx);
                } else if (override) {
                    ecs_entity_t src = it->sources[0];
                    it->sources[0] = 0;
                    flecs_observers_invoke(world, &ider->self, it, table, 0, evtx);
                    flecs_observers_invoke(world, &ider->self_up, it, table, 0, evtx);
                    it->sources[0] = src;
                }
            }

            it->event = event;
            it->ptrs[0] = base_ptr;
        }
    }
}

static
void flecs_emit_forward_and_cache_id(
    ecs_world_t *world,
    ecs_event_record_t *er,
    ecs_event_record_t *er_onset,
    const ecs_type_t *emit_ids,
    ecs_iter_t *it,
    ecs_table_t *table,
    ecs_id_record_t *idr,
    ecs_entity_t tgt,
    ecs_record_t *tgt_record,
    ecs_table_t *tgt_table,
    const ecs_table_record_t *tgt_tr,
    int32_t column,
    int32_t offset,
    ecs_vec_t *reachable_ids,
    ecs_entity_t trav,
    int32_t evtx)
{
    /* Cache forwarded id for (rel, tgt) pair */
    ecs_reachable_elem_t *elem = ecs_vec_append_t(&world->allocator,
        reachable_ids, ecs_reachable_elem_t);
    elem->tr = tgt_tr;
    elem->record = tgt_record;
    elem->src = tgt;
    elem->id = idr->id;
#ifndef NDEBUG
    elem->table = tgt_table;
#endif
    ecs_assert(tgt_table == tgt_record->table, ECS_INTERNAL_ERROR, NULL);

    flecs_emit_forward_id(world, er, er_onset, emit_ids, it, table, idr,
        tgt, tgt_table, column, offset, trav, evtx);
}

static
int32_t flecs_emit_stack_at(
    ecs_vec_t *stack,
    ecs_id_record_t *idr)
{
    int32_t sp = 0, stack_count = ecs_vec_count(stack);
    ecs_table_t **stack_elems = ecs_vec_first(stack);

    for (sp = 0; sp < stack_count; sp ++) {
        ecs_table_t *elem = stack_elems[sp];
        if (flecs_id_record_get_table(idr, elem)) {
            break;
        }
    }

    return sp;
}

static
bool flecs_emit_stack_has(
    ecs_vec_t *stack,
    ecs_id_record_t *idr)
{
    return flecs_emit_stack_at(stack, idr) != ecs_vec_count(stack);
}

static
void flecs_emit_forward_cached_ids(
    ecs_world_t *world,
    ecs_event_record_t *er,
    ecs_event_record_t *er_onset,
    const ecs_type_t *emit_ids,
    ecs_iter_t *it,
    ecs_table_t *table,
    ecs_reachable_cache_t *rc,
    ecs_vec_t *reachable_ids,
    ecs_vec_t *stack,
    ecs_entity_t trav,
    int32_t evtx)
{
    ecs_reachable_elem_t *elems = ecs_vec_first_t(&rc->ids, 
        ecs_reachable_elem_t);
    int32_t i, count = ecs_vec_count(&rc->ids);
    for (i = 0; i < count; i ++) {
        ecs_reachable_elem_t *rc_elem = &elems[i];
        const ecs_table_record_t *rc_tr = rc_elem->tr;
        ecs_assert(rc_tr != NULL, ECS_INTERNAL_ERROR, NULL);
        ecs_id_record_t *rc_idr = (ecs_id_record_t*)rc_tr->hdr.cache;
        ecs_record_t *rc_record = rc_elem->record;

        ecs_assert(rc_idr->id == rc_elem->id, ECS_INTERNAL_ERROR, NULL);
        ecs_assert(rc_record != NULL, ECS_INTERNAL_ERROR, NULL);
        ecs_assert(flecs_entities_get(world, rc_elem->src) == 
            rc_record, ECS_INTERNAL_ERROR, NULL);
        ecs_dbg_assert(rc_record->table == rc_elem->table, 
            ECS_INTERNAL_ERROR, NULL);

        if (flecs_emit_stack_has(stack, rc_idr)) {
            continue;
        }

        int32_t rc_offset = ECS_RECORD_TO_ROW(rc_record->row);
        flecs_emit_forward_and_cache_id(world, er, er_onset, emit_ids,
            it, table, rc_idr, rc_elem->src,
                rc_record, rc_record->table, rc_tr, rc_tr->column,
                    rc_offset, reachable_ids, trav, evtx);
    }
}

static
void flecs_emit_dump_cache(
    ecs_world_t *world,
    const ecs_vec_t *vec)
{
    ecs_reachable_elem_t *elems = ecs_vec_first_t(vec, ecs_reachable_elem_t);
    for (int i = 0; i < ecs_vec_count(vec); i ++) {
        ecs_reachable_elem_t *elem = &elems[i];
        char *idstr = ecs_id_str(world, elem->id);
        char *estr = ecs_id_str(world, elem->src);
        ecs_dbg_3("- id: %s (%u), src: %s (%u), table: %p", 
            idstr, (uint32_t)elem->id,
            estr, (uint32_t)elem->src,
            elem->table);
        ecs_os_free(idstr);
        ecs_os_free(estr);
    }
    if (!ecs_vec_count(vec)) {
        ecs_dbg_3("- no entries");
    }
}

static
void flecs_emit_forward_table_up(
    ecs_world_t *world,
    ecs_event_record_t *er,
    ecs_event_record_t *er_onset,
    const ecs_type_t *emit_ids,
    ecs_iter_t *it,
    ecs_table_t *table,
    ecs_entity_t tgt,
    ecs_table_t *tgt_table,
    ecs_record_t *tgt_record,
    ecs_id_record_t *tgt_idr,
    ecs_vec_t *stack,
    ecs_vec_t *reachable_ids,
    int32_t evtx)
{
    ecs_allocator_t *a = &world->allocator;
    int32_t i, id_count = tgt_table->type.count;
    ecs_id_t *ids = tgt_table->type.array;
    int32_t offset = ECS_RECORD_TO_ROW(tgt_record->row);
    int32_t rc_child_offset = ecs_vec_count(reachable_ids);
    int32_t stack_count = ecs_vec_count(stack);

    /* If tgt_idr is out of sync but is not the current id record being updated,
     * keep track so that we can update two records for the cost of one. */
    ecs_reachable_cache_t *rc = &tgt_idr->reachable;
    bool parent_revalidate = (reachable_ids != &rc->ids) && 
        (rc->current != rc->generation);
    if (parent_revalidate) {
        ecs_vec_reset_t(a, &rc->ids, ecs_reachable_elem_t);
    }

    if (ecs_should_log_3()) {
        char *idstr = ecs_id_str(world, tgt_idr->id);
        ecs_dbg_3("forward events from %s", idstr);
        ecs_os_free(idstr);
    }
    ecs_log_push_3();

    /* Function may have to copy values from overridden components if an IsA
     * relationship was added together with other components. */
    ecs_entity_t trav = ECS_PAIR_FIRST(tgt_idr->id);
    bool inherit = trav == EcsIsA;

    for (i = 0; i < id_count; i ++) {
        ecs_id_t id = ids[i];
        ecs_table_record_t *tgt_tr = &tgt_table->_->records[i];
        ecs_id_record_t *idr = (ecs_id_record_t*)tgt_tr->hdr.cache;
        if (inherit && (idr->flags & EcsIdDontInherit)) {
            continue;
        }

        /* Id has the same relationship, traverse to find ids for forwarding */
        if (ECS_PAIR_FIRST(id) == trav) {
            ecs_table_t **t = ecs_vec_append_t(&world->allocator, stack, 
                ecs_table_t*);
            t[0] = tgt_table;

            ecs_reachable_cache_t *idr_rc = &idr->reachable;
            if (idr_rc->current == idr_rc->generation) {
                /* Cache hit, use cached ids to prevent traversing the same
                 * hierarchy multiple times. This especially speeds up code 
                 * where (deep) hierarchies are created. */
                if (ecs_should_log_3()) {
                    char *idstr = ecs_id_str(world, id);
                    ecs_dbg_3("forward cached for %s", idstr);
                    ecs_os_free(idstr);
                }
                ecs_log_push_3();
                flecs_emit_forward_cached_ids(world, er, er_onset, emit_ids, it,
                    table, idr_rc, reachable_ids, stack, trav, evtx);
                ecs_log_pop_3();
            } else {
                /* Cache is dirty, traverse upwards */
                do {
                    flecs_emit_forward_up(world, er, er_onset, emit_ids, it, 
                        table, idr, stack, reachable_ids, evtx);
                    if (++i >= id_count) {
                        break;
                    }

                    id = ids[i];
                    if (ECS_PAIR_FIRST(id) != trav) {
                        break;
                    }
                } while (true);
            }

            ecs_vec_remove_last(stack);
            continue;
        }

        int32_t stack_at = flecs_emit_stack_at(stack, idr);
        if (parent_revalidate && (stack_at == (stack_count - 1))) {
            /* If parent id record needs to be revalidated, add id */
            ecs_reachable_elem_t *elem = ecs_vec_append_t(a, &rc->ids, 
                ecs_reachable_elem_t);
            elem->tr = tgt_tr;
            elem->record = tgt_record;
            elem->src = tgt;
            elem->id = idr->id;
#ifndef NDEBUG
            elem->table = tgt_table;
#endif
        }

        /* Skip id if it's masked by a lower table in the tree */
        if (stack_at != stack_count) {
            continue;
        }

        flecs_emit_forward_and_cache_id(world, er, er_onset, emit_ids, it,
            table, idr, tgt, tgt_record, tgt_table, tgt_tr, i, 
                offset, reachable_ids, trav, evtx);
    }

    if (parent_revalidate) {
        /* If this is not the current cache being updated, but it's marked
         * as out of date, use intermediate results to populate cache. */
        int32_t rc_parent_offset = ecs_vec_count(&rc->ids);

        /* Only add ids that were added for this table */
        int32_t count = ecs_vec_count(reachable_ids);
        count -= rc_child_offset;

        /* Append ids to any ids that already were added /*/
        if (count) {
            ecs_vec_grow_t(a, &rc->ids, ecs_reachable_elem_t, count);
            ecs_reachable_elem_t *dst = ecs_vec_get_t(&rc->ids, 
                ecs_reachable_elem_t, rc_parent_offset);
            ecs_reachable_elem_t *src = ecs_vec_get_t(reachable_ids,
                ecs_reachable_elem_t, rc_child_offset);
            ecs_os_memcpy_n(dst, src, ecs_reachable_elem_t, count);
        }

        rc->current = rc->generation;

        if (ecs_should_log_3()) {
            char *idstr = ecs_id_str(world, tgt_idr->id);
            ecs_dbg_3("cache revalidated for %s:", idstr);
            ecs_os_free(idstr);
            flecs_emit_dump_cache(world, &rc->ids);
        }
    }

    ecs_log_pop_3();
}

static
void flecs_emit_forward_up(
    ecs_world_t *world,
    ecs_event_record_t *er,
    ecs_event_record_t *er_onset,
    const ecs_type_t *emit_ids,
    ecs_iter_t *it,
    ecs_table_t *table,
    ecs_id_record_t *idr,
    ecs_vec_t *stack,
    ecs_vec_t *reachable_ids,
    int32_t evtx)
{
    ecs_id_t id = idr->id;
    ecs_entity_t tgt = ECS_PAIR_SECOND(id);
    tgt = flecs_entities_get_generation(world, tgt);
    ecs_assert(tgt != 0, ECS_INTERNAL_ERROR, NULL);
    ecs_record_t *tgt_record = flecs_entities_try(world, tgt);
    ecs_table_t *tgt_table;
    if (!tgt_record || !(tgt_table = tgt_record->table)) {
        return;
    }

    flecs_emit_forward_table_up(world, er, er_onset, emit_ids, it, table, 
        tgt, tgt_table, tgt_record, idr, stack, reachable_ids, evtx);
}

static
void flecs_emit_forward(
    ecs_world_t *world,
    ecs_event_record_t *er,
    ecs_event_record_t *er_onset,
    const ecs_type_t *emit_ids,
    ecs_iter_t *it,
    ecs_table_t *table,
    ecs_id_record_t *idr,
    int32_t evtx)
{
    ecs_reachable_cache_t *rc = &idr->reachable;

    if (rc->current != rc->generation) {
        /* Cache miss, iterate the tree to find ids to forward */
        if (ecs_should_log_3()) {
            char *idstr = ecs_id_str(world, idr->id);
            ecs_dbg_3("reachable cache miss for %s", idstr);
            ecs_os_free(idstr);
        }
        ecs_log_push_3();

        ecs_vec_t stack;
        ecs_vec_init_t(&world->allocator, &stack, ecs_table_t*, 0);
        ecs_vec_reset_t(&world->allocator, &rc->ids, ecs_reachable_elem_t);
        flecs_emit_forward_up(world, er, er_onset, emit_ids, it, table, 
            idr, &stack, &rc->ids, evtx);
        it->sources[0] = 0;
        ecs_vec_fini_t(&world->allocator, &stack, ecs_table_t*);

        if (it->event == EcsOnAdd || it->event == EcsOnRemove) {
            /* Only OnAdd/OnRemove events can validate top-level cache, which
             * is for the id for which the event is emitted. 
             * The reason for this is that we don't want to validate the cache
             * while the administration for the mutated entity isn't up to 
             * date yet. */
            rc->current = rc->generation;
        }

        if (ecs_should_log_3()) {
            ecs_dbg_3("cache after rebuild:");
            flecs_emit_dump_cache(world, &rc->ids);
        }

        ecs_log_pop_3();
    } else {
        /* Cache hit, use cached values instead of walking the tree */
        if (ecs_should_log_3()) {
            char *idstr = ecs_id_str(world, idr->id);
            ecs_dbg_3("reachable cache hit for %s", idstr);
            ecs_os_free(idstr);
            flecs_emit_dump_cache(world, &rc->ids);
        }

        ecs_entity_t trav = ECS_PAIR_FIRST(idr->id);
        ecs_reachable_elem_t *elems = ecs_vec_first_t(&rc->ids, 
            ecs_reachable_elem_t);
        int32_t i, count = ecs_vec_count(&rc->ids);
        for (i = 0; i < count; i ++) {
            ecs_reachable_elem_t *elem = &elems[i];
            const ecs_table_record_t *tr = elem->tr;
            ecs_assert(tr != NULL, ECS_INTERNAL_ERROR, NULL);
            ecs_id_record_t *rc_idr = (ecs_id_record_t*)tr->hdr.cache;
            ecs_record_t *r = elem->record;

            ecs_assert(rc_idr->id == elem->id, ECS_INTERNAL_ERROR, NULL);
            ecs_assert(r != NULL, ECS_INTERNAL_ERROR, NULL);
            ecs_assert(flecs_entities_get(world, elem->src) == r,
                ECS_INTERNAL_ERROR, NULL);

            ecs_dbg_assert(r->table == elem->table, ECS_INTERNAL_ERROR, NULL);

            int32_t offset = ECS_RECORD_TO_ROW(r->row);
            flecs_emit_forward_id(world, er, er_onset, emit_ids, it, table,
                rc_idr, elem->src, r->table, tr->column, offset, trav, evtx);
        }
    }
}

/* The emit function is responsible for finding and invoking the observers 
 * matching the emitted event. The function is also capable of forwarding events
 * for newly reachable ids (after adding a relationship) and propagating events
 * downwards. Both capabilities are not just useful in application logic, but
 * are also an important building block for keeping query caches in sync. */
void flecs_emit(
    ecs_world_t *world,
    ecs_world_t *stage,
    ecs_event_desc_t *desc)
{
    ecs_poly_assert(world, ecs_world_t);
    ecs_check(desc != NULL, ECS_INVALID_PARAMETER, NULL);
    ecs_check(desc->event != 0, ECS_INVALID_PARAMETER, NULL);
    ecs_check(desc->event != EcsWildcard, ECS_INVALID_PARAMETER, NULL);
    ecs_check(desc->ids != NULL, ECS_INVALID_PARAMETER, NULL);
    ecs_check(desc->ids->count != 0, ECS_INVALID_PARAMETER, NULL);
    ecs_check(desc->table != NULL, ECS_INVALID_PARAMETER, NULL);
    ecs_check(desc->observable != NULL, ECS_INVALID_PARAMETER, NULL);

    ecs_time_t t = {0};
    bool measure_time = world->flags & EcsWorldMeasureSystemTime;
    if (measure_time) {
        ecs_time_measure(&t);
    }

    const ecs_type_t *ids = desc->ids;
    ecs_entity_t event = desc->event;
    ecs_table_t *table = desc->table;
    int32_t offset = desc->offset;
    int32_t i, r, count = desc->count;
    ecs_flags32_t table_flags = table->flags;

    /* Table events are emitted for internal table operations only, and do not
     * provide component data and/or entity ids. */
    bool table_event = desc->flags & EcsEventTableOnly;
    if (!count && !table_event) {
        /* If no count is provided, forward event for all entities in table */
        count = ecs_table_count(table) - offset;
    }

    /* When the NoOnSet flag is provided, no OnSet/UnSet events should be 
     * generated when new components are inherited. */
    bool no_on_set = desc->flags & EcsEventNoOnSet;

    ecs_id_t ids_cache = 0;
    void *ptrs_cache = NULL;
    ecs_size_t sizes_cache = 0;
    int32_t columns_cache = 0;
    ecs_entity_t sources_cache = 0;

    ecs_iter_t it = {
        .world = stage,
        .real_world = world,
        .event = event,
        .table = table,
        .field_count = 1,
        .ids = &ids_cache,
        .ptrs = &ptrs_cache,
        .sizes = &sizes_cache,
        .columns = &columns_cache,
        .sources = &sources_cache,
        .other_table = desc->other_table,
        .offset = offset,
        .count = count,
        .param = (void*)desc->param,
        .flags = desc->flags | EcsIterIsValid
    };

    /* The world event id is used to determine if an observer has already been
     * triggered for an event. Observers for multiple components are split up
     * into multiple observers for a single component, and this counter is used
     * to make sure a multi observer only triggers once, even if multiple of its
     * single-component observers trigger. */
    int32_t evtx = ++world->event_id;

    ecs_observable_t *observable = ecs_get_observable(desc->observable);
    ecs_check(observable != NULL, ECS_INVALID_PARAMETER, NULL);

    /* Event records contain all observers for a specific event. In addition to
     * the emitted event, also request data for the Wildcard event (for 
     * observers subscribing to the wildcard event), OnSet and UnSet events. The
     * latter to are used for automatically emitting OnSet/UnSet events for 
     * inherited components, for example when an IsA relationship is added to an
     * entity. This doesn't add much overhead, as fetching records is cheap for
     * builtin event types. */
    ecs_event_record_t *er = flecs_event_record_get_if(observable, event);
    ecs_event_record_t *wcer = flecs_event_record_get_if(observable, EcsWildcard);
    ecs_event_record_t *er_onset = flecs_event_record_get_if(observable, EcsOnSet);
    ecs_event_record_t *er_unset = flecs_event_record_get_if(observable, EcsUnSet);

    ecs_data_t *storage = NULL;
    ecs_vec_t *columns = NULL;
    if (count) {
        storage = &table->data;
        columns = storage->columns;
        it.entities = ecs_vec_get_t(&storage->entities, ecs_entity_t, offset);
    }

    int32_t id_count = ids->count;
    ecs_id_t *id_array = ids->array;

    /* If a table has IsA relationships, OnAdd/OnRemove events can trigger 
     * (un)overriding a component. When a component is overridden its value is
     * initialized with the value of the overridden component. */
    bool can_override = count && (table_flags & EcsTableHasIsA) && (
        (event == EcsOnAdd) || (event == EcsOnRemove));

    /* When a new (traversable) relationship is added (emitting an OnAdd/OnRemove
     * event) this will cause the components of the target entity to be 
     * propagated to the source entity. This makes it possible for observers to
     * get notified of any new reachable components though the relationship. */
    bool can_forward = event != EcsOnSet;

    /* Set if event has been propagated */
    bool propagated = false;

    /* Does table has observed entities */
    bool has_observed = table_flags & EcsTableHasTraversable;

    /* When a relationship is removed, the events reachable through that 
     * relationship should emit UnSet events. This is part of the behavior that
     * allows observers to be agnostic of whether a component is inherited. */
    bool can_unset = count && (event == EcsOnRemove) && !no_on_set;

    ecs_event_id_record_t *iders[5] = {0};
    int32_t unset_count = 0;

repeat_event:
    /* This is the core event logic, which is executed for each event. By 
     * default this is just the event kind from the ecs_event_desc_t struct, but
     * can also include the Wildcard and UnSet events. The latter is emitted as
     * counterpart to OnSet, for any removed ids associated with data. */
    for (i = 0; i < id_count; i ++) {
        /* Emit event for each id passed to the function. In most cases this 
         * will just be one id, like a component that was added, removed or set.
         * In some cases events are emitted for multiple ids.
         * 
         * One example is when an id was added with a "With" property, or 
         * inheriting from a prefab with overrides. In these cases an entity is 
         * moved directly to the archetype with the additional components. */
        ecs_id_record_t *idr = NULL;
        const ecs_type_info_t *ti = NULL;
        ecs_id_t id = id_array[i];
        int32_t ider_i, ider_count = 0;
        bool is_pair = ECS_IS_PAIR(id);
        void *override_ptr = NULL;
        ecs_entity_t base = 0;

        /* Check if this id is a pair of an traversable relationship. If so, we 
         * may have to forward ids from the pair's target. */
        if ((can_forward && is_pair) || can_override) {
            idr = flecs_query_id_record_get(world, id);
            ecs_flags32_t idr_flags = idr->flags;

            if (is_pair && (idr_flags & EcsIdTraversable)) {
                ecs_event_record_t *er_fwd = NULL;
                if (ECS_PAIR_FIRST(id) == EcsIsA) {
                    if (event == EcsOnAdd) {
                        if (!world->stages[0].base) {
                            /* Adding an IsA relationship can trigger prefab
                             * instantiation, which can instantiate prefab 
                             * hierarchies for the entity to which the 
                             * relationship was added. */
                            ecs_entity_t tgt = ECS_PAIR_SECOND(id);

                            /* Setting this value prevents flecs_instantiate 
                             * from being called recursively, in case prefab
                             * children also have IsA relationships. */
                            world->stages[0].base = tgt;
                            flecs_instantiate(world, tgt, table, offset, count);
                            world->stages[0].base = 0;
                        }

                        /* Adding an IsA relationship will emit OnSet events for
                         * any new reachable components. */
                        er_fwd = er_onset;
                    } else if (event == EcsOnRemove) {
                        /* Vice versa for removing an IsA relationship. */
                        er_fwd = er_unset;
                    }
                }

                /* Forward events for components from pair target */
                flecs_emit_forward(world, er, er_fwd, ids, &it, table, idr, evtx);
            }

            if (can_override && (!(idr_flags & EcsIdDontInherit))) {
                /* Initialize overridden components with value from base */
                ti = idr->type_info;
                if (ti) {
                    ecs_table_record_t *base_tr = NULL;
                    int32_t base_column = ecs_search_relation(world, table, 
                        0, id, EcsIsA, EcsUp, &base, NULL, &base_tr);
                    if (base_column != -1) {
                        /* Base found with component */
                        ecs_table_t *base_table = base_tr->hdr.table;
                        base_column = ecs_table_type_to_storage_index(
                            base_table, base_tr->column);
                        ecs_assert(base_column != -1, ECS_INTERNAL_ERROR, NULL);
                        ecs_record_t *base_r = flecs_entities_get(world, base);
                        ecs_assert(base_r != NULL, ECS_INTERNAL_ERROR, NULL);
                        int32_t base_row = ECS_RECORD_TO_ROW(base_r->row);
                        ecs_vec_t *base_v = &base_table->data.columns[base_column];
                        override_ptr = ecs_vec_get(base_v, ti->size, base_row);
                    }
                }
            }
        }

        if (er) {
            /* Get observer sets for id. There can be multiple sets of matching
             * observers, in case an observer matches for wildcard ids. For
             * example, both observers for (ChildOf, p) and (ChildOf, *) would
             * match an event for (ChildOf, p). */
            ider_count = flecs_event_observers_get(er, id, iders);
            idr = idr ? idr : flecs_query_id_record_get(world, id);
            ecs_assert(idr != NULL, ECS_INTERNAL_ERROR, NULL);
        }

        if (can_unset) {
            /* Increase UnSet count in case this is a component (has data). This
             * will cause the event loop to be ran again as UnSet event. */
            idr = idr ? idr : flecs_query_id_record_get(world, id);
            ecs_assert(idr != NULL, ECS_INTERNAL_ERROR, NULL);
            unset_count += (idr->type_info != NULL);
        }

        if (!ider_count && !override_ptr) {
            /* If nothing more to do for this id, early out */
            continue;
        }

        ecs_assert(idr != NULL, ECS_INTERNAL_ERROR, NULL);
        const ecs_table_record_t *tr = flecs_id_record_get_table(idr, table);
        if (tr == NULL) {
            /* When a single batch contains multiple add's for an exclusive
             * relationship, it's possible that an id was in the added list
             * that is no longer available for the entity. */
            continue;
        }

        int32_t column = tr->column, storage_i = -1;
        it.columns[0] = column + 1;
        it.ptrs[0] = NULL;
        it.sizes[0] = 0;
        it.event_id = id;
        it.ids[0] = id;

        if (count) {
            storage_i = ecs_table_type_to_storage_index(table, column);
            if (storage_i != -1) {
                /* If this is a component, fetch pointer & size */
                ecs_assert(idr->type_info != NULL, ECS_INTERNAL_ERROR, NULL);
                ecs_vec_t *vec = &columns[storage_i];
                ecs_size_t size = idr->type_info->size;
                void *ptr = ecs_vec_get(vec, size, offset);
                it.sizes[0] = size;

                if (override_ptr) {
                    if (event == EcsOnAdd) {
                        /* If this is a new override, initialize the component
                         * with the value of the overridden component. */
                        flecs_override_copy(
                            world, table, ti, ptr, override_ptr, offset, count);
                    } else if (er_onset) {
                        /* If an override was removed, this re-exposes the
                         * overridden component. Because this causes the actual
                         * (now inherited) value of the component to change, an
                         * OnSet event must be emitted for the base component.*/
                        ecs_assert(event == EcsOnRemove, ECS_INTERNAL_ERROR, NULL);
                        ecs_event_id_record_t *iders_set[5] = {0};
                        int32_t ider_set_i, ider_set_count = 
                            flecs_event_observers_get(er_onset, id, iders_set);
                        if (ider_set_count) {
                            /* Set the source temporarily to the base and base
                             * component pointer. */
                            it.sources[0] = base;
                            it.ptrs[0] = ptr;
                            for (ider_set_i = 0; ider_set_i < ider_set_count; ider_set_i ++) {
                                ecs_event_id_record_t *ider = iders_set[ider_set_i];
                                flecs_observers_invoke(world, &ider->self_up, &it, table, EcsIsA, evtx);
                                flecs_observers_invoke(world, &ider->up, &it, table, EcsIsA, evtx);
                            }
                            it.sources[0] = 0;
                        }
                    }
                }

                it.ptrs[0] = ptr;
            } else {
                if (it.event == EcsUnSet) {
                    /* Only valid for components, not tags */
                    continue;
                }
            }
        }

        /* Actually invoke observers for this event/id */
        for (ider_i = 0; ider_i < ider_count; ider_i ++) {
            ecs_event_id_record_t *ider = iders[ider_i];
            flecs_observers_invoke(world, &ider->self, &it, table, 0, evtx);
            flecs_observers_invoke(world, &ider->self_up, &it, table, 0, evtx);
        }

        if (!ider_count || !count || !has_observed) {
            continue;
        }

        /* If event is propagated, we don't have to manually invalidate entities
         * lower in the tree(s). */
        propagated = true;

        /* The table->traversable_count value indicates if the table contains any
         * entities that are used as targets of traversable relationships. If the
         * entity/entities for which the event was generated is used as such a
         * target, events must be propagated downwards. */
        ecs_entity_t *entities = it.entities;
        it.entities = NULL;

        ecs_record_t **recs = ecs_vec_get_t(&storage->records, 
            ecs_record_t*, offset);
        for (r = 0; r < count; r ++) {
            ecs_record_t *record = recs[r];
            if (!record) {
                /* If the event is emitted after a bulk operation, it's possible
                 * that it hasn't been populated with entities yet. */
                continue;
            }

            ecs_id_record_t *idr_t = record->idr;
            if (idr_t) {
                /* Entity is used as target in traversable pairs, propagate */
                ecs_entity_t e = entities[r];
                it.sources[0] = e;
                flecs_emit_propagate(world, &it, idr, idr_t, iders, ider_count);
            }
        }

        it.table = table;
        it.entities = entities;
        it.count = count;
        it.offset = offset;
        it.sources[0] = 0;
    }

    if (count && can_forward && has_observed && !propagated) {
        flecs_emit_propagate_invalidate(world, table, offset, count);
    }

    can_override = false; /* Don't override twice */
    can_unset = false; /* Don't unset twice */
    can_forward = false; /* Don't forward twice */

    if (unset_count && er_unset && (er != er_unset)) {
        /* Repeat event loop for UnSet event */
        unset_count = 0;
        er = er_unset;
        it.event = EcsUnSet;
        goto repeat_event;
    }

    if (wcer && er != wcer) {
        /* Repeat event loop for Wildcard event */
        er = wcer;
        it.event = event;
        goto repeat_event;
    }

error:
    if (measure_time) {
        world->info.emit_time_total += (ecs_ftime_t)ecs_time_measure(&t);
    }
    return;
}

void ecs_emit(
    ecs_world_t *stage,
    ecs_event_desc_t *desc)
{
    ecs_world_t *world = (ecs_world_t*)ecs_get_world(stage);
    if (desc->entity) {
        ecs_assert(desc->table == NULL, ECS_INVALID_PARAMETER, NULL);
        ecs_assert(desc->offset == 0, ECS_INVALID_PARAMETER, NULL);
        ecs_assert(desc->count == 0, ECS_INVALID_PARAMETER, NULL);
        ecs_record_t *r = flecs_entities_get(world, desc->entity);
        ecs_table_t *table;
        if (!r || !(table = r->table)) {
            /* Empty entities can't trigger observers */
            return;
        }
        desc->table = table;
        desc->offset = ECS_RECORD_TO_ROW(r->row);
        desc->count = 1;
    }
    if (!desc->observable) {
        desc->observable = world;
    }
    flecs_emit(world, stage, desc);
}

/**
 * @file filter.c
 * @brief Uncached query implementation.
 * 
 * Uncached queries (filters) are stateless objects that do not cache their 
 * results. This file contains the creation and validation of uncached queries
 * and code for query iteration.
 * 
 * There file contains the implementation for term queries and filters. Term 
 * queries are uncached queries that only apply to a single term. Filters are
 * uncached queries that support multiple terms. Filters are built on top of
 * term queries: before iteration a filter will first find a "pivot" term (the
 * term with the smallest number of elements), and create a term iterator for
 * it. The output of that term iterator is then evaluated against the rest of
 * the terms of the filter.
 * 
 * Cached queries and observers are built using filters.
 */

#include <ctype.h>

ecs_filter_t ECS_FILTER_INIT = { .hdr = { .magic = ecs_filter_t_magic }};

/* Helper type for passing around context required for error messages */
typedef struct {
    const ecs_world_t *world;
    ecs_filter_t *filter;
    ecs_term_t *term;
    int32_t term_index;
} ecs_filter_finalize_ctx_t;

static
char* flecs_filter_str(
    const ecs_world_t *world,
    const ecs_filter_t *filter,
    const ecs_filter_finalize_ctx_t *ctx,
    int32_t *term_start_out);

static
void flecs_filter_error(
    const ecs_filter_finalize_ctx_t *ctx,
    const char *fmt,
    ...)
{
    va_list args;
    va_start(args, fmt);

    int32_t term_start = 0;

    char *expr = NULL;
    if (ctx->filter) {
        expr = flecs_filter_str(ctx->world, ctx->filter, ctx, &term_start);
    } else {
        expr = ecs_term_str(ctx->world, ctx->term);
    }
    const char *name = NULL;
    if (ctx->filter && ctx->filter->entity) {
        name = ecs_get_name(ctx->filter->world, ctx->filter->entity);
    }
    ecs_parser_errorv(name, expr, term_start, fmt, args);
    ecs_os_free(expr);

    va_end(args);
}

static
int flecs_term_id_finalize_flags(
    ecs_term_id_t *term_id,
    ecs_filter_finalize_ctx_t *ctx)
{
    if ((term_id->flags & EcsIsEntity) && (term_id->flags & EcsIsVariable)) {
        flecs_filter_error(ctx, "cannot set both IsEntity and IsVariable");
        return -1;
    }

    if (!(term_id->flags & (EcsIsEntity|EcsIsVariable|EcsIsName))) {
        if (term_id->id || term_id->name) {
            if (term_id->id == EcsThis || 
                term_id->id == EcsWildcard || 
                term_id->id == EcsAny || 
                term_id->id == EcsVariable) 
            {
                /* Builtin variable ids default to variable */
                term_id->flags |= EcsIsVariable;
            } else {
                term_id->flags |= EcsIsEntity;
            }
        }
    }

    if (term_id->flags & EcsParent) {
        term_id->flags |= EcsUp;
        term_id->trav = EcsChildOf;
    }

    if ((term_id->flags & EcsCascade) && !(term_id->flags & (EcsUp|EcsDown))) {
        term_id->flags |= EcsUp;
    }

    if ((term_id->flags & (EcsUp|EcsDown)) && !term_id->trav) {
        term_id->trav = EcsIsA;
    }

    if (term_id->trav && !(term_id->flags & EcsTraverseFlags)) {
        term_id->flags |= EcsUp;
    }

    return 0;
}

static
int flecs_term_id_lookup(
    const ecs_world_t *world,
    ecs_entity_t scope,
    ecs_term_id_t *term_id,
    bool free_name,
    ecs_filter_finalize_ctx_t *ctx)
{
    char *name = term_id->name;
    if (!name) {
        return 0;
    }

    if (term_id->flags & EcsIsVariable) {
        if (!ecs_os_strcmp(name, "This") || !ecs_os_strcmp(name, "this")) {
            term_id->id = EcsThis;
            if (free_name) {
                ecs_os_free(term_id->name);
            }
            term_id->name = NULL;
        }
        return 0;
    } else if (term_id->flags & EcsIsName) {
        return 0;
    }

    ecs_assert(term_id->flags & EcsIsEntity, ECS_INTERNAL_ERROR, NULL);

    if (ecs_identifier_is_0(name)) {
        if (term_id->id) {
            flecs_filter_error(ctx, "name '0' does not match entity id");
            return -1;
        }
        return 0;
    }

    ecs_entity_t e = ecs_lookup_symbol(world, name, true);
    if (scope && !e) {
        e = ecs_lookup_child(world, scope, name);
    }

    if (!e) {
        if (ctx->filter && (ctx->filter->flags & EcsFilterUnresolvedByName)) {
            term_id->flags |= EcsIsName;
            term_id->flags &= ~EcsIsEntity;
        } else {
            flecs_filter_error(ctx, "unresolved identifier '%s'", name);
            return -1;
        }
    }

    if (term_id->id && term_id->id != e) {
        char *e_str = ecs_get_fullpath(world, term_id->id);
        flecs_filter_error(ctx, "name '%s' does not match term.id '%s'", 
            name, e_str);
        ecs_os_free(e_str);
        return -1;
    }

    term_id->id = e;

    if (!ecs_os_strcmp(name, "*") || !ecs_os_strcmp(name, "_") || 
        !ecs_os_strcmp(name, "$")) 
    {
        term_id->flags &= ~EcsIsEntity;
        term_id->flags |= EcsIsVariable;
    }

    /* Check if looked up id is alive (relevant for numerical ids) */
    if (!(term_id->flags & EcsIsName)) {
        if (!ecs_is_alive(world, term_id->id)) {
            flecs_filter_error(ctx, "identifier '%s' is not alive", term_id->name);
            return -1;
        }

        if (free_name) {
            ecs_os_free(name);
        }

        term_id->name = NULL;
    }

    return 0;
}

static
int flecs_term_ids_finalize(
    const ecs_world_t *world,
    ecs_term_t *term,
    ecs_filter_finalize_ctx_t *ctx)
{
    ecs_term_id_t *src = &term->src;
    ecs_term_id_t *first = &term->first;
    ecs_term_id_t *second = &term->second;

    /* Include inherited components (like from prefabs) by default for src */
    if (!(src->flags & EcsTraverseFlags)) {
        src->flags |= EcsSelf | EcsUp;
    }

    /* Include subsets for component by default, to support inheritance */
    if (!(first->flags & EcsTraverseFlags)) {
        first->flags |= EcsSelf;
        if (first->id && first->flags & EcsIsEntity) {
            if (flecs_id_record_get(world, ecs_pair(EcsIsA, first->id))) {
                first->flags |= EcsDown;
            }
        }
    }

    /* Traverse Self by default for pair target */
    if (!(second->flags & EcsTraverseFlags)) {
        second->flags |= EcsSelf;
    }

    /* Source defaults to This */
    if ((src->id == 0) && (src->name == NULL) && !(src->flags & EcsIsEntity)) {
        src->id = EcsThis;
        src->flags |= EcsIsVariable;
    }

    /* Initialize term identifier flags */
    if (flecs_term_id_finalize_flags(src, ctx)) {
        return -1;
    }
    if (flecs_term_id_finalize_flags(first, ctx)) {
        return -1;
    }

    if (flecs_term_id_finalize_flags(second, ctx)) {
        return -1;
    }

    /* Lookup term identifiers by name */
    if (flecs_term_id_lookup(world, 0, src, term->move, ctx)) {
        return -1;
    }
    if (flecs_term_id_lookup(world, 0, first, term->move, ctx)) {
        return -1;
    }

    ecs_entity_t first_id = 0;
    ecs_entity_t oneof = 0;
    if (first->flags & EcsIsEntity) {
        first_id = first->id;

        /* If first element of pair has OneOf property, lookup second element of
         * pair in the value of the OneOf property */
        oneof = flecs_get_oneof(world, first_id);
    }

    if (flecs_term_id_lookup(world, oneof, &term->second, term->move, ctx)) {
        return -1;
    }

    /* If source is 0, reset traversal flags */
    if (src->id == 0 && src->flags & EcsIsEntity) {
        src->flags &= ~EcsTraverseFlags;
        src->trav = 0;
    }
    /* If second is 0, reset traversal flags */
    if (second->id == 0 && second->flags & EcsIsEntity) {
        second->flags &= ~EcsTraverseFlags;
        second->trav = 0;
    }

    /* If source is wildcard, term won't return any data */
    if ((src->flags & EcsIsVariable) && ecs_id_is_wildcard(src->id)) {
        term->inout |= EcsInOutNone;
    }

    return 0;
}

static
ecs_entity_t flecs_term_id_get_entity(
    const ecs_term_id_t *term_id)
{
    if (term_id->flags & EcsIsEntity) {
        return term_id->id; /* Id is known */
    } else if (term_id->flags & EcsIsVariable) {
        /* Return wildcard for variables, as they aren't known yet */
        if (term_id->id != EcsAny) {
            /* Any variable should not use wildcard, as this would return all
             * ids matching a wildcard, whereas Any returns the first match */
            return EcsWildcard;
        } else {
            return EcsAny;
        }
    } else {
        return 0; /* Term id is uninitialized */
    }
}

static
int flecs_term_populate_id(
    ecs_term_t *term,
    ecs_filter_finalize_ctx_t *ctx)
{
    ecs_entity_t first = flecs_term_id_get_entity(&term->first);
    ecs_entity_t second = flecs_term_id_get_entity(&term->second);
    ecs_id_t role = term->id_flags;

    if (first & ECS_ID_FLAGS_MASK) {
        return -1;
    }
    if (second & ECS_ID_FLAGS_MASK) {
        return -1;
    }

    if ((second || term->second.flags == EcsIsEntity) && !role) {
        role = term->id_flags = ECS_PAIR;
    }

    if (!second && !ECS_HAS_ID_FLAG(role, PAIR)) {
        term->id = first | role;
    } else {
        if (!ECS_HAS_ID_FLAG(role, PAIR)) {
            flecs_filter_error(ctx, "invalid role for pair");
            return -1;
        }

        term->id = ecs_pair(first, second);
    }

    return 0;
}

static
int flecs_term_populate_from_id(
    const ecs_world_t *world,
    ecs_term_t *term,
    ecs_filter_finalize_ctx_t *ctx)
{
    ecs_entity_t first = 0;
    ecs_entity_t second = 0;
    ecs_id_t role = term->id & ECS_ID_FLAGS_MASK;

    if (!role && term->id_flags) {
        role = term->id_flags;
        term->id |= role;
    }

    if (term->id_flags && term->id_flags != role) {
        flecs_filter_error(ctx, "mismatch between term.id & term.id_flags");
        return -1;
    }

    term->id_flags = role;

    if (ECS_HAS_ID_FLAG(term->id, PAIR)) {
        first = ECS_PAIR_FIRST(term->id);
        second = ECS_PAIR_SECOND(term->id);

        if (!first) {
            flecs_filter_error(ctx, "missing first element in term.id");
            return -1;
        }
        if (!second) {
            if (first != EcsChildOf) {
                flecs_filter_error(ctx, "missing second element in term.id");
                return -1;
            } else {
                /* (ChildOf, 0) is allowed so filter can be used to efficiently
                 * query for root entities */
            }
        }
    } else {
        first = term->id & ECS_COMPONENT_MASK;
        if (!first) {
            flecs_filter_error(ctx, "missing first element in term.id");
            return -1;
        }
    }

    ecs_entity_t term_first = flecs_term_id_get_entity(&term->first);
    if (term_first) {
        if ((uint32_t)term_first != first) {
            flecs_filter_error(ctx, "mismatch between term.id and term.first");
            return -1;
        }
    } else {
        if (!(term->first.id = ecs_get_alive(world, first))) {
            term->first.id = first;
        }
    }

    ecs_entity_t term_second = flecs_term_id_get_entity(&term->second);
    if (term_second) {
        if ((uint32_t)term_second != second) {
            flecs_filter_error(ctx, "mismatch between term.id and term.second");
            return -1;
        }
    } else if (second) {
        if (!(term->second.id = ecs_get_alive(world, second))) {
            term->second.id = second;
        }
    }

    return 0;
}

static
int flecs_term_verify_eq_pred(
    const ecs_term_t *term,
    ecs_filter_finalize_ctx_t *ctx)
{
    ecs_entity_t first_id = term->first.id;
    const ecs_term_id_t *second = &term->second;
    const ecs_term_id_t *src = &term->src;

    if (term->oper != EcsAnd && term->oper != EcsNot && term->oper != EcsOr) {
        flecs_filter_error(ctx, "invalid operator combination");
        goto error;
    }

    if ((src->flags & EcsIsName) && (second->flags & EcsIsName)) {
        flecs_filter_error(ctx, "both sides of operator cannot be a name");
        goto error;
    }

    if ((src->flags & EcsIsEntity) && (second->flags & EcsIsEntity)) {
        flecs_filter_error(ctx, "both sides of operator cannot be an entity");
        goto error;
    }

    if (!(src->flags & EcsIsVariable)) {
        flecs_filter_error(ctx, "left-hand of operator must be a variable");
        goto error;
    }

    if (first_id == EcsPredMatch && !(second->flags & EcsIsName)) {
        flecs_filter_error(ctx, "right-hand of match operator must be a string");
        goto error;
    }

    if ((src->flags & EcsIsVariable) && (second->flags & EcsIsVariable)) {
        if (src->id && src->id == second->id) {
            flecs_filter_error(ctx, "both sides of operator are equal");
            goto error;
        }
        if (src->name && second->name && !ecs_os_strcmp(src->name, second->name)) {
            flecs_filter_error(ctx, "both sides of operator are equal");
            goto error;
        }
    }

    return 0;
error:
    return -1;
}

static
int flecs_term_verify(
    const ecs_world_t *world,
    const ecs_term_t *term,
    ecs_filter_finalize_ctx_t *ctx)
{
    const ecs_term_id_t *first = &term->first;
    const ecs_term_id_t *second = &term->second;
    const ecs_term_id_t *src = &term->src;
    ecs_entity_t first_id = 0, second_id = 0;
    ecs_id_t role = term->id_flags;
    ecs_id_t id = term->id;

    if ((src->flags & EcsIsName) && (second->flags & EcsIsName)) {
        flecs_filter_error(ctx, "mismatch between term.id_flags & term.id");
        return -1;
    }

    if (first->flags & EcsIsEntity) {
        first_id = first->id;
    }
    if (second->flags & EcsIsEntity) {
        second_id = second->id;
    }

    if (first_id == EcsPredEq || first_id == EcsPredMatch || first_id == EcsPredLookup) {
        return flecs_term_verify_eq_pred(term, ctx);
    }

    if (role != (id & ECS_ID_FLAGS_MASK)) {
        flecs_filter_error(ctx, "mismatch between term.id_flags & term.id");
        return -1;
    }

    if (ecs_term_id_is_set(second) && !ECS_HAS_ID_FLAG(role, PAIR)) {
        flecs_filter_error(ctx, "expected PAIR flag for term with pair");
        return -1;
    } else if (!ecs_term_id_is_set(second) && ECS_HAS_ID_FLAG(role, PAIR)) {
        if (first_id != EcsChildOf) {
            flecs_filter_error(ctx, "unexpected PAIR flag for term without pair");
            return -1;
        } else {
            /* Exception is made for ChildOf so we can use (ChildOf, 0) to match
             * all entities in the root */
        }
    }

    if (!ecs_term_id_is_set(src)) {
        flecs_filter_error(ctx, "term.src is not initialized");
        return -1;
    }

    if (!ecs_term_id_is_set(first)) {
        flecs_filter_error(ctx, "term.first is not initialized");
        return -1;
    }

    if (ECS_HAS_ID_FLAG(role, PAIR)) {
        if (!ECS_PAIR_FIRST(id)) {
            flecs_filter_error(ctx, "invalid 0 for first element in pair id");
            return -1;
        }
        if ((ECS_PAIR_FIRST(id) != EcsChildOf) && !ECS_PAIR_SECOND(id)) {
            flecs_filter_error(ctx, "invalid 0 for second element in pair id");
            return -1;
        }

        if ((first->flags & EcsIsEntity) && 
            (ecs_entity_t_lo(first_id) != ECS_PAIR_FIRST(id))) 
        {
            flecs_filter_error(ctx, "mismatch between term.id and term.first");
            return -1;
        }
        if ((first->flags & EcsIsVariable) && 
            !ecs_id_is_wildcard(ECS_PAIR_FIRST(id)))
        {
            char *id_str = ecs_id_str(world, id);
            flecs_filter_error(ctx, 
                "expected wildcard for variable term.first (got %s)", id_str);
            ecs_os_free(id_str);
            return -1;
        }

        if ((second->flags & EcsIsEntity) && 
            (ecs_entity_t_lo(second_id) != ECS_PAIR_SECOND(id))) 
        {
            flecs_filter_error(ctx, "mismatch between term.id and term.second");
            return -1;
        }
        if ((second->flags & EcsIsVariable) && 
            !ecs_id_is_wildcard(ECS_PAIR_SECOND(id))) 
        {
            char *id_str = ecs_id_str(world, id);
            flecs_filter_error(ctx, 
                "expected wildcard for variable term.second (got %s)", id_str);
            ecs_os_free(id_str);
            return -1;
        }
    } else {
        ecs_entity_t component = id & ECS_COMPONENT_MASK;
        if (!component) {
            flecs_filter_error(ctx, "missing component id");
            return -1;
        }
        if ((first->flags & EcsIsEntity) && 
            (ecs_entity_t_lo(first_id) != ecs_entity_t_lo(component))) 
        {
            flecs_filter_error(ctx, "mismatch between term.id and term.first");
            return -1;
        }
        if ((first->flags & EcsIsVariable) && !ecs_id_is_wildcard(component)) {
            char *id_str = ecs_id_str(world, id);
            flecs_filter_error(ctx, 
                "expected wildcard for variable term.first (got %s)", id_str);
            ecs_os_free(id_str);
            return -1;
        }
    }

    if (first_id) {
        if (ecs_term_id_is_set(second)) {
            ecs_flags32_t mask = EcsIsEntity | EcsIsVariable;
            if ((src->flags & mask) == (second->flags & mask)) {
                bool is_same = false;
                if (src->flags & EcsIsEntity) {
                    is_same = src->id == second->id;
                } else if (src->name && second->name) {
                    is_same = !ecs_os_strcmp(src->name, second->name);
                }

                if (is_same && ecs_has_id(world, first_id, EcsAcyclic)
                    && !(term->flags & EcsTermReflexive)) 
                {
                    char *pred_str = ecs_get_fullpath(world, term->first.id);
                    flecs_filter_error(ctx, "term with acyclic relationship"
                        " '%s' cannot have same subject and object",
                            pred_str);
                    ecs_os_free(pred_str);
                    return -1;
                }
            }
        }

        if (second_id && !ecs_id_is_wildcard(second_id)) {
            ecs_entity_t oneof = flecs_get_oneof(world, first_id);
            if (oneof) {
                if (!ecs_has_pair(world, second_id, EcsChildOf, oneof)) {
                    char *second_str = ecs_get_fullpath(world, second_id);
                    char *oneof_str = ecs_get_fullpath(world, oneof);
                    char *id_str = ecs_id_str(world, term->id);
                    flecs_filter_error(ctx, 
                        "invalid target '%s' for %s: must be child of '%s'",
                            second_str, id_str, oneof_str);
                    ecs_os_free(second_str);
                    ecs_os_free(oneof_str);
                    ecs_os_free(id_str);
                    return -1;
                }
            }
        }
    }

    if (term->src.trav) {
        if (!ecs_has_id(world, term->src.trav, EcsTraversable)) {
            char *r_str = ecs_get_fullpath(world, term->src.trav);
            flecs_filter_error(ctx, 
                "cannot traverse non-traversable relationship '%s'", r_str);
            ecs_os_free(r_str);
            return -1;
        }
    }

    return 0;
}

static
int flecs_term_finalize(
    const ecs_world_t *world,
    ecs_term_t *term,
    ecs_filter_finalize_ctx_t *ctx)
{
    ctx->term = term;

    ecs_term_id_t *src = &term->src;
    ecs_term_id_t *first = &term->first;
    ecs_term_id_t *second = &term->second;
    ecs_flags32_t first_flags = first->flags;
    ecs_flags32_t src_flags = src->flags;
    ecs_flags32_t second_flags = second->flags;

    if (term->id) {
        if (flecs_term_populate_from_id(world, term, ctx)) {
            return -1;
        }
    }

    if (flecs_term_ids_finalize(world, term, ctx)) {
        return -1;
    }

    if ((first->flags & EcsIsVariable) && (term->first.id == EcsAny)) {
        term->flags |= EcsTermMatchAny;
    }
    if ((second->flags & EcsIsVariable) && (term->second.id == EcsAny)) {
        term->flags |= EcsTermMatchAny;
    }
    if ((src->flags & EcsIsVariable) && (term->src.id == EcsAny)) {
        term->flags |= EcsTermMatchAnySrc;
    }

    /* If EcsVariable is used by itself, assign to predicate (singleton) */
    if ((src->id == EcsVariable) && (src->flags & EcsIsVariable)) {
        src->id = first->id;
        src->flags &= ~(EcsIsVariable | EcsIsEntity);
        src->flags |= first->flags & (EcsIsVariable | EcsIsEntity);
    }
    if ((second->id == EcsVariable) && (second->flags & EcsIsVariable)) {
        second->id = first->id;
        second->flags &= ~(EcsIsVariable | EcsIsEntity);
        second->flags |= first->flags & (EcsIsVariable | EcsIsEntity);
    }

    ecs_flags32_t mask = EcsIsEntity | EcsIsVariable;
    if ((src->flags & mask) == (second->flags & mask)) {
        bool is_same = false;
        if (src->flags & EcsIsEntity) {
            is_same = src->id == second->id;
        } else if (src->name && second->name) {
            is_same = !ecs_os_strcmp(src->name, second->name);
        }
        if (is_same) {
            term->flags |= EcsTermSrcSecondEq;
        }
    }
    if ((src->flags & mask) == (first->flags & mask)) {
        bool is_same = false;
        if (src->flags & EcsIsEntity) {
            is_same = src->id == first->id;
        } else if (src->name && first->name) {
            is_same = !ecs_os_strcmp(src->name, first->name);
        }
        if (is_same) {
            term->flags |= EcsTermSrcFirstEq;
        }
    }

    if (!term->id) {
        if (flecs_term_populate_id(term, ctx)) {
            return -1;
        }
    }

    /* If term queries for !(ChildOf, _), translate it to the builtin 
     * (ChildOf, 0) index which is a cheaper way to find root entities */
    if (term->oper == EcsNot && term->id == ecs_pair(EcsChildOf, EcsAny)) {
        term->oper = EcsAnd;
        term->id = ecs_pair(EcsChildOf, 0);
        term->second.id = 0;
        term->second.flags |= EcsIsEntity;
        term->second.flags &= ~EcsIsVariable;
    }

    ecs_entity_t first_id = 0;
    if (term->first.flags & EcsIsEntity) {
        first_id = term->first.id;
    }

    term->idr = flecs_query_id_record_get(world, term->id);
    ecs_flags32_t id_flags = term->idr ? term->idr->flags : 0;

    if (first_id) {
        ecs_entity_t first_trav = first->trav;

        /* If component is inherited from, set correct traversal flags */
        ecs_flags32_t first_trav_flags = first_flags & EcsTraverseFlags;
        if (!first_trav && first_trav_flags != EcsSelf) {
            /* Inheritance uses IsA by default, but can use any relationship */
            first_trav = EcsIsA;
        }

        ecs_record_t *trav_record = NULL;
        ecs_table_t *trav_table = NULL;
        if (first_trav) {
            trav_record = flecs_entities_get(world, first_trav);
            trav_table = trav_record ? trav_record->table : NULL;
            if (first_trav != EcsIsA) {
                if (!trav_table || !ecs_table_has_id(world, trav_table, EcsTraversable)) {
                    flecs_filter_error(ctx, "first.trav is not traversable");
                    return -1;
                }
            }
        }

        /* Only enable inheritance for ids which are inherited from at the time
         * of filter creation. To force component inheritance to be evaluated,
         * an application can explicitly set traversal flags. */
        if ((first_trav_flags & EcsDown) || 
            flecs_id_record_get(world, ecs_pair(first_trav, first->id))) 
        {
            if (first_trav_flags == EcsSelf) {
                flecs_filter_error(ctx, "first.trav specified with self");
                return -1;
            }

            if (!first_trav_flags || (first_trav_flags & EcsDown)) {
                term->flags |= EcsTermIdInherited;
                first->trav = first_trav;
                if (!first_trav_flags) {
                    first->flags &= ~EcsTraverseFlags;
                    first->flags |= EcsDown;
                    ecs_assert(trav_table != NULL, ECS_INTERNAL_ERROR, NULL);
                    if ((first_trav == EcsIsA) || ecs_table_has_id(
                        world, trav_table, EcsReflexive)) 
                    {
                        first->flags |= EcsSelf;
                    }
                }
            }
        }

        /* Don't traverse ids that cannot be inherited */
        if ((id_flags & EcsIdDontInherit) && (src->trav == EcsIsA)) {
            if (src_flags & (EcsUp | EcsDown)) {
                flecs_filter_error(ctx, 
                    "traversing not allowed for id that can't be inherited");
                return -1;
            }
            src->flags &= ~(EcsUp | EcsDown);
            src->trav = 0;
        }

        /* If component id is final, don't attempt component inheritance */
        ecs_record_t *first_record = flecs_entities_get(world, first_id);
        ecs_table_t *first_table = first_record ? first_record->table : NULL;
        if (first_table) {
            if (ecs_table_has_id(world, first_table, EcsFinal)) {
                if (first_flags & EcsDown) {
                    flecs_filter_error(ctx, "final id cannot be traversed down");
                    return -1;
                }
            }

            /* Add traversal flags for transitive relationships */
            if (!(second_flags & EcsTraverseFlags) && ecs_term_id_is_set(second)) {
                if (!((src->flags & EcsIsVariable) && (src->id == EcsAny))) {
                    if (!((second->flags & EcsIsVariable) && (second->id == EcsAny))) {
                        if (ecs_table_has_id(world, first_table, EcsTransitive)) {
                            second->flags |= EcsSelf|EcsUp|EcsTraverseAll;
                            second->trav = first_id;
                            term->flags |= EcsTermTransitive;
                        }
                    }
                }
            }

            if (ecs_table_has_id(world, first_table, EcsReflexive)) {
                term->flags |= EcsTermReflexive;
            }
        }
    }

    if (first->id == EcsVariable) {
        flecs_filter_error(ctx, "invalid $ for term.first");
        return -1;
    }

    if (term->id_flags & ECS_AND) {
        term->oper = EcsAndFrom;
        term->id &= ECS_COMPONENT_MASK;
        term->id_flags = 0;
    }

    if (term->oper == EcsAndFrom || term->oper == EcsOrFrom || term->oper == EcsNotFrom) {
        if (term->inout != EcsInOutDefault && term->inout != EcsInOutNone) {
            flecs_filter_error(ctx, 
                "invalid inout value for AndFrom/OrFrom/NotFrom term");
            return -1;
        }
    }

    if (flecs_term_verify(world, term, ctx)) {
        return -1;
    }

    return 0;
}

ecs_id_t flecs_to_public_id(
    ecs_id_t id)
{
    if (ECS_PAIR_FIRST(id) == EcsUnion) {
        return ecs_pair(ECS_PAIR_SECOND(id), EcsWildcard);
    } else {
        return id;
    }
}

ecs_id_t flecs_from_public_id(
    ecs_world_t *world,
    ecs_id_t id)
{
    if (ECS_HAS_ID_FLAG(id, PAIR)) {
        ecs_entity_t first = ECS_PAIR_FIRST(id);
        ecs_id_record_t *idr = flecs_id_record_ensure(world, 
            ecs_pair(first, EcsWildcard));
        if (idr->flags & EcsIdUnion) {
            return ecs_pair(EcsUnion, first);
        }
    }

    return id;
}

bool ecs_identifier_is_0(
    const char *id)
{
    return id[0] == '0' && !id[1];
}

bool ecs_id_match(
    ecs_id_t id,
    ecs_id_t pattern)
{
    if (id == pattern) {
        return true;
    }

    if (ECS_HAS_ID_FLAG(pattern, PAIR)) {
        if (!ECS_HAS_ID_FLAG(id, PAIR)) {
            return false;
        }

        ecs_entity_t id_rel = ECS_PAIR_FIRST(id);
        ecs_entity_t id_obj = ECS_PAIR_SECOND(id);
        ecs_entity_t pattern_rel = ECS_PAIR_FIRST(pattern);
        ecs_entity_t pattern_obj = ECS_PAIR_SECOND(pattern);

        ecs_check(id_rel != 0, ECS_INVALID_PARAMETER, NULL);
        ecs_check(id_obj != 0, ECS_INVALID_PARAMETER, NULL);

        ecs_check(pattern_rel != 0, ECS_INVALID_PARAMETER, NULL);
        ecs_check(pattern_obj != 0, ECS_INVALID_PARAMETER, NULL);
        
        if (pattern_rel == EcsWildcard) {
            if (pattern_obj == EcsWildcard || pattern_obj == id_obj) {
                return true;
            }
        } else if (pattern_rel == EcsFlag) {
            /* Used for internals, helps to keep track of which ids are used in
             * pairs that have additional flags (like OVERRIDE and TOGGLE) */
            if (ECS_HAS_ID_FLAG(id, PAIR) && !ECS_IS_PAIR(id)) {
                if (ECS_PAIR_FIRST(id) == pattern_obj) {
                    return true;
                }
                if (ECS_PAIR_SECOND(id) == pattern_obj) {
                    return true;
                }
            }
        } else if (pattern_obj == EcsWildcard) {
            if (pattern_rel == id_rel) {
                return true;
            }
        }
    } else {
        if ((id & ECS_ID_FLAGS_MASK) != (pattern & ECS_ID_FLAGS_MASK)) {
            return false;
        }

        if ((ECS_COMPONENT_MASK & pattern) == EcsWildcard) {
            return true;
        }
    }

error:
    return false;
}

bool ecs_id_is_pair(
    ecs_id_t id)
{
    return ECS_HAS_ID_FLAG(id, PAIR);
}

bool ecs_id_is_wildcard(
    ecs_id_t id)
{
    if ((id == EcsWildcard) || (id == EcsAny)) {
        return true;
    }

    bool is_pair = ECS_IS_PAIR(id);
    if (!is_pair) {
        return false;
    }

    ecs_entity_t first = ECS_PAIR_FIRST(id);
    ecs_entity_t second = ECS_PAIR_SECOND(id);

    return (first == EcsWildcard) || (second == EcsWildcard) ||
           (first == EcsAny) || (second == EcsAny);
}

bool ecs_id_is_valid(
    const ecs_world_t *world,
    ecs_id_t id)
{
    if (!id) {
        return false;
    }
    if (ecs_id_is_wildcard(id)) {
        return false;
    }

    if (ECS_HAS_ID_FLAG(id, PAIR)) {
        if (!ECS_PAIR_FIRST(id)) {
            return false;
        }
        if (!ECS_PAIR_SECOND(id)) {
            return false;
        }
    } else if (id & ECS_ID_FLAGS_MASK) {
        if (!ecs_is_valid(world, id & ECS_COMPONENT_MASK)) {
            return false;
        }
    }

    return true;
}

ecs_flags32_t ecs_id_get_flags(
    const ecs_world_t *world,
    ecs_id_t id)
{
    ecs_id_record_t *idr = flecs_id_record_get(world, id);
    if (idr) {
        return idr->flags;
    } else {
        return 0;
    }
}

bool ecs_term_id_is_set(
    const ecs_term_id_t *id)
{
    return id->id != 0 || id->name != NULL || id->flags & EcsIsEntity;
}

bool ecs_term_is_initialized(
    const ecs_term_t *term)
{
    return term->id != 0 || ecs_term_id_is_set(&term->first);
}

bool ecs_term_match_this(
    const ecs_term_t *term)
{
    return (term->src.flags & EcsIsVariable) && (term->src.id == EcsThis);
}

bool ecs_term_match_0(
    const ecs_term_t *term)
{
    return (term->src.id == 0) && (term->src.flags & EcsIsEntity);
}

int ecs_term_finalize(
    const ecs_world_t *world,
    ecs_term_t *term)
{
    ecs_filter_finalize_ctx_t ctx = {0};
    ctx.world = world;
    ctx.term = term;
    return flecs_term_finalize(world, term, &ctx);
}

ecs_term_t ecs_term_copy(
    const ecs_term_t *src)
{
    ecs_term_t dst = *src;
    dst.name = ecs_os_strdup(src->name);
    dst.first.name = ecs_os_strdup(src->first.name);
    dst.src.name = ecs_os_strdup(src->src.name);
    dst.second.name = ecs_os_strdup(src->second.name);
    return dst;
}

ecs_term_t ecs_term_move(
    ecs_term_t *src)
{
    if (src->move) {
        ecs_term_t dst = *src;
        src->name = NULL;
        src->first.name = NULL;
        src->src.name = NULL;
        src->second.name = NULL;
        dst.move = false;
        return dst;
    } else {
        ecs_term_t dst = ecs_term_copy(src);
        dst.move = false;
        return dst;
    }
}

void ecs_term_fini(
    ecs_term_t *term)
{
    ecs_os_free(term->first.name);
    ecs_os_free(term->src.name);
    ecs_os_free(term->second.name);
    ecs_os_free(term->name);

    term->first.name = NULL;
    term->src.name = NULL;
    term->second.name = NULL;
    term->name = NULL;
}

static
ecs_term_t* flecs_filter_or_other_type(
    ecs_filter_t *f,
    int32_t t)
{
    ecs_term_t *term = &f->terms[t];
    ecs_term_t *first = NULL;
    while (t--) {
        if (f->terms[t].oper != EcsOr) {
            break;
        }
        first = &f->terms[t];
    }

    if (first) {
        ecs_world_t *world = f->world;
        const ecs_type_info_t *first_type;
        if (first->idr) {
            first_type = first->idr->type_info;
        } else {
            first_type = ecs_get_type_info(world, first->id);
        }
        const ecs_type_info_t *term_type;
        if (term->idr) {
            term_type = term->idr->type_info;
        } else {
            term_type = ecs_get_type_info(world, term->id);
        }

        if (first_type == term_type) {
            return NULL;
        }
        return first;
    } else {
        return NULL;
    }
}

int ecs_filter_finalize(
    const ecs_world_t *world,
    ecs_filter_t *f)
{
    int32_t i, term_count = f->term_count, field_count = 0;
    ecs_term_t *terms = f->terms;
    int32_t filter_terms = 0, scope_nesting = 0;
    bool cond_set = false;

    ecs_filter_finalize_ctx_t ctx = {0};
    ctx.world = world;
    ctx.filter = f;

    f->flags |= EcsFilterMatchOnlyThis;
    for (i = 0; i < term_count; i ++) {
        ecs_term_t *term = &terms[i];
        ctx.term_index = i;
        if (flecs_term_finalize(world, term, &ctx)) {
            return -1;
        }

        if (i && term[-1].oper == EcsOr) {
            if (term[-1].src.id != term->src.id) {
                flecs_filter_error(&ctx, "mismatching src.id for OR terms");
                return -1;
            }
            if (term->oper != EcsOr && term->oper != EcsAnd) {
                flecs_filter_error(&ctx, 
                    "term after OR operator must use AND operator");
                return -1;
            }
        } else {
            field_count ++;
        }

        if (term->oper == EcsOr || (i && term[-1].oper == EcsOr)) {
            ecs_term_t *first = flecs_filter_or_other_type(f, i);
            if (first) {
                filter_terms ++;
                if (first == &term[-1]) {
                    filter_terms ++;
                }
            }
        }

        term->field_index = field_count - 1;

        if (ecs_term_match_this(term)) {
            ECS_BIT_SET(f->flags, EcsFilterMatchThis);
        } else {
            ECS_BIT_CLEAR(f->flags, EcsFilterMatchOnlyThis);
        }

        if (term->id == EcsPrefab) {
            ECS_BIT_SET(f->flags, EcsFilterMatchPrefab);
        }
        if (term->id == EcsDisabled && (term->src.flags & EcsSelf)) {
            ECS_BIT_SET(f->flags, EcsFilterMatchDisabled);
        }

        if (ECS_BIT_IS_SET(f->flags, EcsFilterNoData)) {
            term->inout = EcsInOutNone;
        }
        
        if (term->oper == EcsNot && term->inout == EcsInOutDefault) {
            term->inout = EcsInOutNone;
        }

        if (term->inout == EcsInOutNone) {
            filter_terms ++;
        } else if (term->idr) {
            if (!term->idr->type_info && !(term->idr->flags & EcsIdUnion)) {
                filter_terms ++;
            }
        } else if (ecs_id_is_tag(world, term->id)) {
            if (!ecs_id_is_union(world, term->id)) {
                /* Union ids aren't filters because they return their target
                 * as component value with type ecs_entity_t */
                filter_terms ++;
            }
        }
        if ((term->id == EcsWildcard) || (term->id == 
            ecs_pair(EcsWildcard, EcsWildcard))) 
        {
            /* If term type is unknown beforehand, default the inout type to
             * none. This prevents accidentally requesting lots of components,
             * which can put stress on serializer code. */
            if (term->inout == EcsInOutDefault) {
                term->inout = EcsInOutNone;
            }
        }

        if (term->oper != EcsNot || !ecs_term_match_this(term)) {
            ECS_BIT_CLEAR(f->flags, EcsFilterMatchAnything);
        }

        if (term->idr) {
            if (ecs_os_has_threading()) {
                ecs_os_ainc(&term->idr->keep_alive);
            } else {
                term->idr->keep_alive ++;
            }
        }

        if (term->oper == EcsOptional || term->oper == EcsNot) {
            cond_set = true;
        }

        if (term->first.id == EcsPredEq || term->first.id == EcsPredMatch ||
            term->first.id == EcsPredLookup)
        {
            f->flags |= EcsFilterHasPred;
        }

        if (term->first.id == EcsScopeOpen) {
            f->flags |= EcsFilterHasScopes;
            scope_nesting ++;
        }
        if (term->first.id == EcsScopeClose) {
            if (i && terms[i - 1].first.id == EcsScopeOpen) {
                flecs_filter_error(&ctx, "invalid empty scope");
                return -1;
            }

            f->flags |= EcsFilterHasScopes;
            scope_nesting --;
        }
        if (scope_nesting < 0) {
            flecs_filter_error(&ctx, "'}' without matching '{'");
        }
    }

    if (scope_nesting != 0) {
        flecs_filter_error(&ctx, "missing '}'");
        return -1;
    }

    if (term_count && (terms[term_count - 1].oper == EcsOr)) {
        flecs_filter_error(&ctx, "last term of filter can't have OR operator");
        return -1;
    }

    f->field_count = field_count;

    if (field_count) {
        for (i = 0; i < term_count; i ++) {
            ecs_term_t *term = &terms[i];
            ecs_id_record_t *idr = term->idr;
            int32_t field = term->field_index;

            if (term->oper == EcsOr || (i && (term[-1].oper == EcsOr))) {
                if (flecs_filter_or_other_type(f, i)) {
                    f->sizes[field] = 0;
                    continue;
                }
            }

            if (idr) {
                if (!ECS_IS_PAIR(idr->id) || ECS_PAIR_FIRST(idr->id) != EcsWildcard) {
                    if (idr->flags & EcsIdUnion) {
                        f->sizes[field] = ECS_SIZEOF(ecs_entity_t);
                    } else if (idr->type_info) {
                        f->sizes[field] = idr->type_info->size;
                    }
                }
            } else {
                bool is_union = false;
                if (ECS_IS_PAIR(term->id)) {
                    ecs_entity_t first = ecs_pair_first(world, term->id);
                    if (ecs_has_id(world, first, EcsUnion)) {
                        is_union = true;
                    }
                }
                if (is_union) {
                    f->sizes[field] = ECS_SIZEOF(ecs_entity_t);
                } else {
                    const ecs_type_info_t *ti = ecs_get_type_info(
                        world, term->id);
                    if (ti) {
                        f->sizes[field] = ti->size;
                    }
                }
            }
        }
    } else {
        f->sizes = NULL;
    }

    if (filter_terms >= term_count) {
        ECS_BIT_SET(f->flags, EcsFilterNoData);
    }

    ECS_BIT_COND(f->flags, EcsFilterHasCondSet, cond_set);

    return 0;
}

/* Implementation for iterable mixin */
static
void flecs_filter_iter_init(
    const ecs_world_t *world,
    const ecs_poly_t *poly,
    ecs_iter_t *iter,
    ecs_term_t *filter)
{
    ecs_poly_assert(poly, ecs_filter_t);

    if (filter) {
        iter[1] = ecs_filter_iter(world, (ecs_filter_t*)poly);
        iter[0] = ecs_term_chain_iter(&iter[1], filter);
    } else {
        iter[0] = ecs_filter_iter(world, (ecs_filter_t*)poly);
    }
}

/* Implementation for dtor mixin */
static
void flecs_filter_fini(
    ecs_filter_t *filter)
{
    if (filter->terms) {
        int i, count = filter->term_count;
        for (i = 0; i < count; i ++) {
            ecs_term_t *term = &filter->terms[i];
            if (term->idr) {
                if (!(filter->world->flags & EcsWorldQuit)) {
                    if (ecs_os_has_threading()) {
                        ecs_os_adec(&term->idr->keep_alive);
                    } else {
                        term->idr->keep_alive --;
                    }
                }
            }
            ecs_term_fini(&filter->terms[i]);
        }

        if (filter->terms_owned) {
            /* Memory allocated for both terms & sizes */
            ecs_os_free(filter->terms);
        } else {
            ecs_os_free(filter->sizes);
        }
    }

    filter->terms = NULL;

    if (filter->owned) {
        ecs_os_free(filter);
    }
}

void ecs_filter_fini(
    ecs_filter_t *filter) 
{
    if (filter->owned && filter->entity) {
        /* If filter is associated with entity, use poly dtor path */
        ecs_delete(filter->world, filter->entity);
    } else {
        flecs_filter_fini(filter);
    }
}

ecs_filter_t* ecs_filter_init(
    ecs_world_t *world,
    const ecs_filter_desc_t *desc)    
{
    ecs_check(world != NULL, ECS_INVALID_PARAMETER, NULL);
    ecs_check(desc != NULL, ECS_INVALID_PARAMETER, NULL);
    ecs_check(desc->_canary == 0, ECS_INVALID_PARAMETER, NULL);
    flecs_stage_from_world(&world);

    ecs_filter_t *f = desc->storage;
    int32_t i, term_count = desc->terms_buffer_count, storage_count = 0, expr_count = 0;
    const ecs_term_t *terms = desc->terms_buffer;
    ecs_term_t *storage_terms = NULL, *expr_terms = NULL;

    if (f) {
        ecs_check(f->hdr.magic == ecs_filter_t_magic, 
            ECS_INVALID_PARAMETER, NULL);
        storage_count = f->term_count;
        storage_terms = f->terms;
        ecs_poly_init(f, ecs_filter_t);
    } else {
        f = ecs_poly_new(ecs_filter_t);
        f->owned = true;
    }
    if (!storage_terms) {
        f->terms_owned = true;
    }

    ECS_BIT_COND(f->flags, EcsFilterIsInstanced, desc->instanced);
    ECS_BIT_SET(f->flags, EcsFilterMatchAnything);
    f->flags |= desc->flags;
    f->world = world;

    /* If terms_buffer was not set, count number of initialized terms in
     * static desc::terms array */
    if (!terms) {
        ecs_check(term_count == 0, ECS_INVALID_PARAMETER, NULL);
        terms = desc->terms;
        for (i = 0; i < FLECS_TERM_DESC_MAX; i ++) {
            if (!ecs_term_is_initialized(&terms[i])) {
                break;
            }
            term_count ++;
        }
    } else {
        ecs_check(term_count != 0, ECS_INVALID_PARAMETER, NULL);
    }

    /* If expr is set, parse query expression */
    const char *expr = desc->expr;
    ecs_entity_t entity = desc->entity;
    if (expr) {
#ifdef FLECS_PARSER
        const char *name = NULL;
        const char *ptr = desc->expr;
        ecs_term_t term = {0};
        int32_t expr_size = 0;

        if (entity) {
            name = ecs_get_name(world, entity);
        }

        while (ptr[0] && (ptr = ecs_parse_term(world, name, expr, ptr, &term))){
            if (!ecs_term_is_initialized(&term)) {
                break;
            }

            if (expr_count == expr_size) {
                expr_size = expr_size ? expr_size * 2 : 8;
                expr_terms = ecs_os_realloc_n(expr_terms, ecs_term_t, expr_size);
            }

            expr_terms[expr_count ++] = term;
            if (ptr[0] == '\n') {
                break;
            }
        }

        if (!ptr) {
            /* Set terms in filter object to make sur they get cleaned up */
            f->terms = expr_terms;
            f->term_count = expr_count;
            f->terms_owned = true;
            goto error;
        }
#else
        (void)expr;
        ecs_abort(ECS_UNSUPPORTED, "parser addon is not available");
#endif
    }

    /* If storage is provided, make sure it's large enough */
    ecs_check(!storage_terms || storage_count >= (term_count + expr_count),
        ECS_INVALID_PARAMETER, NULL);

    if (term_count || expr_count) {
        /* Allocate storage for terms and sizes array */
        if (!storage_terms) {
            ecs_assert(f->terms_owned == true, ECS_INTERNAL_ERROR, NULL);
            f->term_count = term_count + expr_count;
            ecs_size_t terms_size = ECS_SIZEOF(ecs_term_t) * f->term_count;
            ecs_size_t sizes_size = ECS_SIZEOF(int32_t) * f->term_count;
            f->terms = ecs_os_calloc(terms_size + sizes_size);
            f->sizes = ECS_OFFSET(f->terms, terms_size);
        } else {
            f->terms = storage_terms;
            f->term_count = storage_count;
            f->sizes = ecs_os_calloc_n(ecs_size_t, term_count);
        }

        /* Copy terms to filter storage */
        for (i = 0; i < term_count; i ++) {
            f->terms[i] = ecs_term_copy(&terms[i]);
            /* Allow freeing resources from expr parser during finalization */
            f->terms[i].move = true;
        }

        /* Move expr terms to filter storage */
        for (i = 0; i < expr_count; i ++) {
            f->terms[i + term_count] = ecs_term_move(&expr_terms[i]);
            /* Allow freeing resources from expr parser during finalization */
            f->terms[i + term_count].move = true;
        }
        ecs_os_free(expr_terms);
    }

    /* Ensure all fields are consistent and properly filled out */
    if (ecs_filter_finalize(world, f)) {
        goto error;
    }

    /* Any allocated resources remaining in terms are now owned by filter */
    for (i = 0; i < f->term_count; i ++) {
        f->terms[i].move = false;
    }

    f->variable_names[0] = NULL;
    f->iterable.init = flecs_filter_iter_init;
    f->dtor = (ecs_poly_dtor_t)flecs_filter_fini;
    f->entity = entity;

    if (entity && f->owned) {
        EcsPoly *poly = ecs_poly_bind(world, entity, ecs_filter_t);
        poly->poly = f;
        ecs_poly_modified(world, entity, ecs_filter_t);
    }

    return f;
error:
    ecs_filter_fini(f);
    return NULL;
}

void ecs_filter_copy(
    ecs_filter_t *dst,
    const ecs_filter_t *src)   
{
    if (src == dst) {
        return;
    }

    if (src) {
        *dst = *src;

        int32_t i, term_count = src->term_count;
        ecs_size_t terms_size = ECS_SIZEOF(ecs_term_t) * term_count;
        ecs_size_t sizes_size = ECS_SIZEOF(int32_t) * term_count;
        dst->terms = ecs_os_malloc(terms_size + sizes_size);
        dst->sizes = ECS_OFFSET(dst->terms, terms_size);
        dst->terms_owned = true;
        ecs_os_memcpy_n(dst->sizes, src->sizes, int32_t, term_count);

        for (i = 0; i < term_count; i ++) {
            dst->terms[i] = ecs_term_copy(&src->terms[i]);
        }
    } else {
        ecs_os_memset_t(dst, 0, ecs_filter_t);
    }
}

void ecs_filter_move(
    ecs_filter_t *dst,
    ecs_filter_t *src)   
{
    if (src == dst) {
        return;
    }

    if (src) {
        *dst = *src;
        if (src->terms_owned) {
            dst->terms = src->terms;
            dst->sizes = src->sizes;
            dst->terms_owned = true;
        } else {
            ecs_filter_copy(dst, src);
        }
        src->terms = NULL;
        src->sizes = NULL;
        src->term_count = 0;
    } else {
        ecs_os_memset_t(dst, 0, ecs_filter_t);
    }
}

static
void flecs_filter_str_add_id(
    const ecs_world_t *world,
    ecs_strbuf_t *buf,
    const ecs_term_id_t *id,
    bool is_subject,
    ecs_flags32_t default_traverse_flags)
{
    bool is_added = false;
    if (!is_subject || id->id != EcsThis) {
        if (id->flags & EcsIsVariable && !ecs_id_is_wildcard(id->id)) {
            ecs_strbuf_appendlit(buf, "$");
        }
        if (id->id) {
            char *path = ecs_get_fullpath(world, id->id);
            ecs_strbuf_appendstr(buf, path);
            ecs_os_free(path);
        } else if (id->name) {
            ecs_strbuf_appendstr(buf, id->name);
        } else {
            ecs_strbuf_appendlit(buf, "0");
        }
        is_added = true;
    }

    ecs_flags32_t flags = id->flags;
    if (!(flags & EcsTraverseFlags)) {
        /* If flags haven't been set yet, initialize with defaults. This can
         * happen if an error is thrown while the term is being finalized */
        flags |= default_traverse_flags;
    }

    if ((flags & EcsTraverseFlags) != default_traverse_flags) {
        if (is_added) {
            ecs_strbuf_list_push(buf, ":", "|");
        } else {
            ecs_strbuf_list_push(buf, "", "|");
        }
        if (id->flags & EcsSelf) {
            ecs_strbuf_list_appendstr(buf, "self");
        }
        if (id->flags & EcsUp) {
            ecs_strbuf_list_appendstr(buf, "up");
        }
        if (id->flags & EcsDown) {
            ecs_strbuf_list_appendstr(buf, "down");
        }

        if (id->trav && (id->trav != EcsIsA)) {
            ecs_strbuf_list_push(buf, "(", "");

            char *rel_path = ecs_get_fullpath(world, id->trav);
            ecs_strbuf_appendstr(buf, rel_path);
            ecs_os_free(rel_path);

            ecs_strbuf_list_pop(buf, ")");
        }

        ecs_strbuf_list_pop(buf, "");
    }
}

static
void flecs_term_str_w_strbuf(
    const ecs_world_t *world,
    const ecs_term_t *term,
    ecs_strbuf_t *buf,
    int32_t t)
{
    const ecs_term_id_t *src = &term->src;
    const ecs_term_id_t *second = &term->second;

    uint8_t def_src_mask = EcsSelf|EcsUp;
    uint8_t def_first_mask = EcsSelf;
    uint8_t def_second_mask = EcsSelf;

    bool pred_set = ecs_term_id_is_set(&term->first);
    bool subj_set = !ecs_term_match_0(term);
    bool obj_set = ecs_term_id_is_set(second);

    if (term->first.id == EcsScopeOpen) {
        ecs_strbuf_appendlit(buf, "{");
        return;
    } else if (term->first.id == EcsScopeClose) {
        ecs_strbuf_appendlit(buf, "}");
        return;
    }

    if (!t || !(term[-1].oper == EcsOr)) {
        if (term->inout == EcsIn) {
            ecs_strbuf_appendlit(buf, "[in] ");
        } else if (term->inout == EcsInOut) {
            ecs_strbuf_appendlit(buf, "[inout] ");
        } else if (term->inout == EcsOut) {
            ecs_strbuf_appendlit(buf, "[out] ");
        } else if (term->inout == EcsInOutNone && term->oper != EcsNot) {
            ecs_strbuf_appendlit(buf, "[none] ");
        }
    }

    if (term->first.flags & EcsIsEntity && term->first.id != 0) {
        if (ecs_has_id(world, term->first.id, EcsDontInherit)) {
            def_src_mask = EcsSelf;
        }
    }

    if (term->oper == EcsNot) {
        ecs_strbuf_appendlit(buf, "!");
    } else if (term->oper == EcsOptional) {
        ecs_strbuf_appendlit(buf, "?");
    }

    if (!subj_set) {
        flecs_filter_str_add_id(world, buf, &term->first, false, 
            def_first_mask);
        if (!obj_set) {
            ecs_strbuf_appendlit(buf, "()");
        } else {
            ecs_strbuf_appendlit(buf, "(0,");
            flecs_filter_str_add_id(world, buf, &term->second, false, 
                def_second_mask);
            ecs_strbuf_appendlit(buf, ")");
        }
    } else if (ecs_term_match_this(term) && 
        (src->flags & EcsTraverseFlags) == def_src_mask)
    {
        if (pred_set) {
            if (obj_set) {
                ecs_strbuf_appendlit(buf, "(");
            }
            flecs_filter_str_add_id(world, buf, &term->first, false, def_first_mask);   
            if (obj_set) {
                ecs_strbuf_appendlit(buf, ",");
                flecs_filter_str_add_id(
                    world, buf, &term->second, false, def_second_mask);
                ecs_strbuf_appendlit(buf, ")");
            }
        } else if (term->id) {
            char *str = ecs_id_str(world, term->id);
            ecs_strbuf_appendstr(buf, str);
            ecs_os_free(str);
        }
    } else {
        if (term->id_flags && !ECS_HAS_ID_FLAG(term->id_flags, PAIR)) {
            ecs_strbuf_appendstr(buf, ecs_id_flag_str(term->id_flags));
            ecs_strbuf_appendch(buf, '|');
        }

        flecs_filter_str_add_id(world, buf, &term->first, false, def_first_mask);
        ecs_strbuf_appendlit(buf, "(");
        if (term->src.flags & EcsIsEntity && term->src.id == term->first.id) {
            ecs_strbuf_appendlit(buf, "$");
        } else {
            flecs_filter_str_add_id(world, buf, &term->src, true, def_src_mask);
        }
        if (obj_set) {
            ecs_strbuf_appendlit(buf, ",");
            flecs_filter_str_add_id(world, buf, &term->second, false, def_second_mask);
        }
        ecs_strbuf_appendlit(buf, ")");
    }
}

char* ecs_term_str(
    const ecs_world_t *world,
    const ecs_term_t *term)
{
    ecs_strbuf_t buf = ECS_STRBUF_INIT;
    flecs_term_str_w_strbuf(world, term, &buf, 0);
    return ecs_strbuf_get(&buf);
}

static
char* flecs_filter_str(
    const ecs_world_t *world,
    const ecs_filter_t *filter,
    const ecs_filter_finalize_ctx_t *ctx,
    int32_t *term_start_out)
{
    ecs_check(world != NULL, ECS_INVALID_PARAMETER, NULL);
    ecs_check(filter != NULL, ECS_INVALID_PARAMETER, NULL);

    ecs_strbuf_t buf = ECS_STRBUF_INIT;
    ecs_term_t *terms = filter->terms;
    int32_t i, count = filter->term_count;

    for (i = 0; i < count; i ++) {
        ecs_term_t *term = &terms[i];

        if (term_start_out && ctx) {
            if (ctx->term_index == i) {
                term_start_out[0] = ecs_strbuf_written(&buf);
                if (i) {
                    term_start_out[0] += 2; /* whitespace  + , */
                }
            }
        }

        flecs_term_str_w_strbuf(world, term, &buf, i);

        if (i != (count - 1)) {
            if (term->oper == EcsOr) {
                ecs_strbuf_appendlit(&buf, " || ");
            } else {
                if (term->first.id != EcsScopeOpen) {
                    if (term[1].first.id != EcsScopeClose) {
                        ecs_strbuf_appendlit(&buf, ", ");
                    }
                }
            }
        }
    }

    return ecs_strbuf_get(&buf);
error:
    return NULL;
}

char* ecs_filter_str(
    const ecs_world_t *world,
    const ecs_filter_t *filter)
{
    return flecs_filter_str(world, filter, NULL, NULL);
}

int32_t ecs_filter_find_this_var(
    const ecs_filter_t *filter)
{
    ecs_check(filter != NULL, ECS_INVALID_PARAMETER, NULL);
    
    if (ECS_BIT_IS_SET(filter->flags, EcsFilterMatchThis)) {
        /* Filters currently only support the This variable at index 0. Only
         * return 0 if filter actually has terms for the This variable. */
        return 0;
    }

error:
    return -1;
}

/* Check if the id is a pair that has Any as first or second element. Any 
 * pairs behave just like Wildcard pairs and reuses the same data structures,
 * with as only difference that the number of results returned for an Any pair
 * is never more than one. This function is used to tell the difference. */
static
bool is_any_pair(
    ecs_id_t id)
{
    if (!ECS_HAS_ID_FLAG(id, PAIR)) {
        return false;
    }

    if (ECS_PAIR_FIRST(id) == EcsAny) {
        return true;
    }
    if (ECS_PAIR_SECOND(id) == EcsAny) {
        return true;
    }

    return false;
}

static
bool flecs_n_term_match_table(
    ecs_world_t *world,
    const ecs_term_t *term,
    const ecs_table_t *table,
    ecs_entity_t type_id,
    ecs_oper_kind_t oper,
    ecs_id_t *id_out,
    int32_t *column_out,
    ecs_entity_t *subject_out,
    int32_t *match_index_out,
    bool first,
    ecs_flags32_t iter_flags)
{
    (void)column_out;

    const ecs_type_t *type = ecs_get_type(world, type_id);
    ecs_assert(type != NULL, ECS_INTERNAL_ERROR, NULL);

    ecs_id_t *ids = type->array;
    int32_t i, count = type->count;
    ecs_term_t temp = *term;
    temp.oper = EcsAnd;

    for (i = 0; i < count; i ++) {
        ecs_id_t id = ids[i];
        if (ecs_id_get_flags(world, id) & EcsIdDontInherit) {
            continue;
        }
        bool result;
        if (ECS_HAS_ID_FLAG(id, AND)) {
            ecs_oper_kind_t id_oper = EcsAndFrom;
            result = flecs_n_term_match_table(world, term, table, 
                id & ECS_COMPONENT_MASK, id_oper, id_out, column_out, 
                subject_out, match_index_out, first, iter_flags);
        } else {
            temp.id = id;
            result = flecs_term_match_table(world, &temp, table, id_out, 
                0, subject_out, match_index_out, first, iter_flags);
        }
        if (!result && oper == EcsAndFrom) {
            return false;
        } else
        if (result && oper == EcsOrFrom) {
            return true;
        }
    }

    if (oper == EcsAndFrom) {
        if (id_out) {
            id_out[0] = type_id;
        }
        return true;
    } else
    if (oper == EcsOrFrom) {
        return false;
    }

    return false;
}

bool flecs_term_match_table(
    ecs_world_t *world,
    const ecs_term_t *term,
    const ecs_table_t *table,
    ecs_id_t *id_out,
    int32_t *column_out,
    ecs_entity_t *subject_out,
    int32_t *match_index_out,
    bool first,
    ecs_flags32_t iter_flags)
{
    const ecs_term_id_t *src = &term->src;
    ecs_oper_kind_t oper = term->oper;
    const ecs_table_t *match_table = table;
    ecs_id_t id = term->id;

    ecs_entity_t src_id = src->id;
    if (ecs_term_match_0(term)) {
        if (id_out) {
            id_out[0] = id; /* If no entity is matched, just set id */
        }
        return true;
    }

    if (oper == EcsAndFrom || oper == EcsOrFrom) {
        return flecs_n_term_match_table(world, term, table, term->id, 
            term->oper, id_out, column_out, subject_out, match_index_out, first, 
            iter_flags);
    }

    /* If source is not This, search in table of source */
    if (!ecs_term_match_this(term)) {
        if (iter_flags & EcsIterEntityOptional) {
            /* Treat entity terms as optional */
            oper = EcsOptional;
        }

        match_table = ecs_get_table(world, src_id);
        if (match_table) {
        } else if (oper != EcsOptional) {
            return false;
        }
    } else {
        /* If filter contains This terms, a table must be provided */
        ecs_assert(table != NULL, ECS_INTERNAL_ERROR, NULL);
    }

    if (!match_table) {
        return false;
    }

    ecs_entity_t source = 0;

    /* If first = false, we're searching from an offset. This supports returning
     * multiple results when using wildcard filters. */
    int32_t column = 0;
    if (!first && column_out && column_out[0] != 0) {
        column = column_out[0];
        if (column < 0) {
            /* In case column is not from This, flip sign */
            column = -column;
        }

        /* Remove base 1 offset */
        column --;
    }

    /* Find location, source and id of match in table type */
    ecs_table_record_t *tr = 0;
    bool is_any = is_any_pair(id);

    column = flecs_search_relation_w_idr(world, match_table,
        column, id, src->trav, src->flags, &source, id_out, &tr, term->idr);

    if (tr && match_index_out) {
        if (!is_any) {
            match_index_out[0] = tr->count;
        } else {
            match_index_out[0] = 1;
        }
    }

    bool result = column != -1;

    if (oper == EcsNot) {
        if (match_index_out) {
            match_index_out[0] = 1;
        }
        result = !result;
    }

    if (oper == EcsOptional) {
        result = true;
    }

    if ((column == -1) && (src->flags & EcsUp) && (table->flags & EcsTableHasTarget)) {
        ecs_assert(table->_ != NULL, ECS_INTERNAL_ERROR, NULL);
        ecs_id_t rel = ECS_PAIR_SECOND(table->type.array[table->_->ft_offset]);
        if (rel == (uint32_t)src->trav) {
            result = true;
        }
    }

    if (!result) {
        if (iter_flags & EcsFilterPopulate) {
            column = 0;
        } else {
            return false;
        }
    }

    if (!ecs_term_match_this(term)) {
        if (!source) {
            source = src_id;
        }
    }

    if (id_out && column < 0) {
        id_out[0] = id;
    }

    if (column_out) {
        if (column >= 0) {
            column ++;
            if (source != 0) {
                column *= -1; 
            }
            column_out[0] = column;
        } else {
            column_out[0] = 0;
        }
    }

    if (subject_out) {
        subject_out[0] = source;
    }

    return result;
}

bool flecs_filter_match_table(
    ecs_world_t *world,
    const ecs_filter_t *filter,
    const ecs_table_t *table,
    ecs_id_t *ids,
    int32_t *columns,
    ecs_entity_t *sources,
    int32_t *match_indices,
    int32_t *matches_left,
    bool first,
    int32_t skip_term,
    ecs_flags32_t iter_flags)
{
    ecs_term_t *terms = filter->terms;
    int32_t i, count = filter->term_count;
    int32_t match_count = 1;
    bool result = true;

    if (matches_left) {
        match_count = *matches_left;
    }

    for (i = 0; i < count; i ++) {
        ecs_term_t *term = &terms[i];
        ecs_oper_kind_t oper = term->oper;
        if (i == skip_term) {
            if (oper != EcsAndFrom && oper != EcsOrFrom && oper != EcsNotFrom) {
                continue;
            }
        }

        ecs_term_id_t *src = &term->src;
        const ecs_table_t *match_table = table;
        int32_t t_i = term->field_index;

        ecs_entity_t src_id = src->id;
        if (!src_id) {
            if (ids) {
                ids[t_i] = term->id;
            }
            continue;
        }

        if (!ecs_term_match_this(term)) {
            match_table = ecs_get_table(world, src_id);
        } else {
            if (ECS_BIT_IS_SET(iter_flags, EcsIterIgnoreThis)) {
                continue;
            }
            
            /* If filter contains This terms, table must be provided */
            ecs_assert(table != NULL, ECS_INTERNAL_ERROR, NULL);
        }

        int32_t match_index = 0;
        if (!i || term[-1].oper != EcsOr) {
            result = false;
        } else {
            if (result) {
                continue; /* Already found matching OR term */
            }
        }

        bool term_result = flecs_term_match_table(world, term, match_table,
            ids ? &ids[t_i] : NULL, 
            columns ? &columns[t_i] : NULL, 
            sources ? &sources[t_i] : NULL, 
            &match_index,
            first,
            iter_flags);

        if (i && term[-1].oper == EcsOr) {
            result |= term_result;
        } else {
            result = term_result;
        }

        if (oper != EcsOr && !result) {
            return false;
        }

        if (first && match_index) {
            match_count *= match_index;
        }
        if (match_indices) {
            match_indices[t_i] = match_index;
        }
    }

    if (matches_left) {
        *matches_left = match_count;
    }

    return true;
}

static
void term_iter_init_no_data(
    ecs_term_iter_t *iter)
{
    iter->term = (ecs_term_t){ .field_index = -1 };
    iter->self_index = NULL;
    iter->index = 0;
}

static
void term_iter_init_w_idr(
    const ecs_term_t *term,
    ecs_term_iter_t *iter, 
    ecs_id_record_t *idr, 
    bool empty_tables)
{
    if (idr) {
        if (empty_tables) {
            flecs_table_cache_all_iter(&idr->cache, &iter->it);
        } else {
            flecs_table_cache_iter(&idr->cache, &iter->it);
        }
    } else {
        term_iter_init_no_data(iter);
    }

    iter->index = 0;
    iter->empty_tables = empty_tables;
    iter->size = 0;
    if (term && term->idr && term->idr->type_info) {
        iter->size = term->idr->type_info->size;
    }
}

static
void term_iter_init_wildcard(
    const ecs_world_t *world,
    ecs_term_iter_t *iter,
    bool empty_tables)
{
    iter->term = (ecs_term_t){ .field_index = -1 };
    iter->self_index = flecs_id_record_get(world, EcsAny);
    ecs_id_record_t *idr = iter->cur = iter->self_index;
    term_iter_init_w_idr(NULL, iter, idr, empty_tables);
}

static
void term_iter_init(
    const ecs_world_t *world,
    ecs_term_t *term,
    ecs_term_iter_t *iter,
    bool empty_tables)
{    
    const ecs_term_id_t *src = &term->src;

    iter->term = *term;

    if (src->flags & EcsSelf) {
        iter->self_index = term->idr;
        if (!iter->self_index) {
            iter->self_index = flecs_query_id_record_get(world, term->id);
        }
    }

    if (src->flags & EcsUp) {
        iter->set_index = flecs_id_record_get(world, 
            ecs_pair(src->trav, EcsWildcard));
    }

    ecs_id_record_t *idr;
    if (iter->self_index) {
        idr = iter->cur = iter->self_index;
    } else {
        idr = iter->cur = iter->set_index;
    }

    term_iter_init_w_idr(term, iter, idr, empty_tables);
}

ecs_iter_t ecs_term_iter(
    const ecs_world_t *stage,
    ecs_term_t *term)
{
    ecs_check(stage != NULL, ECS_INVALID_PARAMETER, NULL);
    ecs_check(term != NULL, ECS_INVALID_PARAMETER, NULL);

    const ecs_world_t *world = ecs_get_world(stage);

    flecs_process_pending_tables(world);

    if (ecs_term_finalize(world, term)) {
        ecs_throw(ECS_INVALID_PARAMETER, NULL);
    }

    ecs_iter_t it = {
        .real_world = (ecs_world_t*)world,
        .world = (ecs_world_t*)stage,
        .field_count = 1,
        .next = ecs_term_next
    };

    /* Term iter populates the iterator with arrays from its own cache, ensure 
     * they don't get overwritten by flecs_iter_validate.
     *
     * Note: the reason the term iterator doesn't use the iterator cache itself
     * (which could easily accomodate a single term) is that the filter iterator
     * is built on top of the term iterator. The private cache of the term
     * iterator keeps the filter iterator code simple, as it doesn't need to
     * worry about the term iter overwriting the iterator fields. */
    flecs_iter_init(stage, &it, 0);
    term_iter_init(world, term, &it.priv.iter.term, false);
    ECS_BIT_COND(it.flags, EcsIterNoData, it.priv.iter.term.size == 0);

    return it;
error:
    return (ecs_iter_t){ 0 };
}

ecs_iter_t ecs_term_chain_iter(
    const ecs_iter_t *chain_it,
    ecs_term_t *term)
{
    ecs_check(chain_it != NULL, ECS_INVALID_PARAMETER, NULL);
    ecs_check(term != NULL, ECS_INVALID_PARAMETER, NULL);

    ecs_world_t *world = chain_it->real_world;
    ecs_check(world != NULL, ECS_INVALID_PARAMETER, NULL);

    if (ecs_term_finalize(world, term)) {
        ecs_throw(ECS_INVALID_PARAMETER, NULL);
    }

    ecs_iter_t it = {
        .real_world = (ecs_world_t*)world,
        .world = chain_it->world,
        .terms = term,
        .field_count = 1,
        .chain_it = (ecs_iter_t*)chain_it,
        .next = ecs_term_next
    };

    flecs_iter_init(chain_it->world, &it, flecs_iter_cache_all);

    term_iter_init(world, term, &it.priv.iter.term, false);

    return it;
error:
    return (ecs_iter_t){ 0 };
}

ecs_iter_t ecs_children(
    const ecs_world_t *world,
    ecs_entity_t parent)
{
    return ecs_term_iter(world,  &(ecs_term_t){ .id = ecs_childof(parent) });
}

bool ecs_children_next(
    ecs_iter_t *it)
{
    return ecs_term_next(it);
}

static
const ecs_table_record_t *flecs_term_iter_next_table(
    ecs_term_iter_t *iter)
{
    ecs_id_record_t *idr = iter->cur;
    if (!idr) {
        return NULL;
    }

    return flecs_table_cache_next(&iter->it, ecs_table_record_t);
}

static
bool flecs_term_iter_find_superset(
    ecs_world_t *world, 
    ecs_table_t *table, 
    ecs_term_t *term, 
    ecs_entity_t *source, 
    ecs_id_t *id, 
    int32_t *column)
{
    ecs_term_id_t *src = &term->src;

    /* Test if following the relationship finds the id */
    int32_t index = flecs_search_relation_w_idr(world, table, 0, 
        term->id, src->trav, src->flags, source, id, 0, term->idr);

    if (index == -1) {
        *source = 0;
        return false;
    }

    ecs_assert(*source != 0, ECS_INTERNAL_ERROR, NULL);

    *column = (index + 1) * -1;

    return true;
}

static
bool flecs_term_iter_next(
    ecs_world_t *world,
    ecs_term_iter_t *iter,
    bool match_prefab,
    bool match_disabled)
{
    ecs_table_t *table = iter->table;
    ecs_entity_t source = 0;
    const ecs_table_record_t *tr;
    ecs_term_t *term = &iter->term;

    do {
        if (table) {
            iter->cur_match ++;
            if (iter->cur_match >= iter->match_count) {
                table = NULL;
            } else {
                iter->last_column = ecs_search_offset(
                    world, table, iter->last_column + 1, term->id, 0);
                iter->column = iter->last_column + 1;
                if (iter->last_column >= 0) {
                    iter->id = table->type.array[iter->last_column];
                }
            }
        }

        if (!table) {
            if (!(tr = flecs_term_iter_next_table(iter))) {
                if (iter->cur != iter->set_index && iter->set_index != NULL) {
                    if (iter->observed_table_count != 0) {
                        iter->cur = iter->set_index;
                        if (iter->empty_tables) {
                            flecs_table_cache_all_iter(
                                &iter->set_index->cache, &iter->it);
                        } else {
                            flecs_table_cache_iter(
                                &iter->set_index->cache, &iter->it);
                        }
                        iter->index = 0;
                        tr = flecs_term_iter_next_table(iter);
                    }
                }

                if (!tr) {
                    return false;
                }
            }

            table = tr->hdr.table;
            if (table->_->traversable_count) {
                iter->observed_table_count ++;
            }

            if (!match_prefab && (table->flags & EcsTableIsPrefab)) {
                continue;
            }

            if (!match_disabled && (table->flags & EcsTableIsDisabled)) {
                continue;
            }

            iter->table = table;
            iter->match_count = tr->count;
            if (is_any_pair(term->id)) {
                iter->match_count = 1;
            }

            iter->cur_match = 0;
            iter->last_column = tr->column;
            iter->column = tr->column + 1;
            iter->id = flecs_to_public_id(table->type.array[tr->column]);
        }

        if (iter->cur == iter->set_index) {
            if (iter->self_index) {
                if (flecs_id_record_get_table(iter->self_index, table) != NULL) {
                    /* If the table has the id itself and this term matched Self
                     * we already matched it */
                    continue;
                }
            }

            if (!flecs_term_iter_find_superset(
                world, table, term, &source, &iter->id, &iter->column)) 
            {
                continue;
            }

            /* The tr->count field refers to the number of relationship instances,
             * not to the number of matches. Superset terms can only yield a
             * single match. */
            iter->match_count = 1;
        }

        break;
    } while (true);

    iter->subject = source;

    return true;
}

static
bool flecs_term_iter_set_table(
    ecs_world_t *world,
    ecs_term_iter_t *iter,
    ecs_table_t *table)
{
    const ecs_table_record_t *tr = NULL;
    const ecs_id_record_t *idr = iter->self_index;
    if (idr) {
        tr = ecs_table_cache_get(&idr->cache, table);
        if (tr) {
            iter->match_count = tr->count;
            iter->last_column = tr->column;
            iter->column = tr->column + 1;
            iter->id = flecs_to_public_id(table->type.array[tr->column]);
        }
    }

    if (!tr) {
        idr = iter->set_index;
        if (idr) {
            tr = ecs_table_cache_get(&idr->cache, table);
            if (!flecs_term_iter_find_superset(world, table, &iter->term, 
                &iter->subject, &iter->id, &iter->column)) 
            {
                return false;
            }
            iter->match_count = 1;
        }
    }

    if (!tr) {
        return false;
    }

    /* Populate fields as usual */
    iter->table = table;
    iter->cur_match = 0;

    return true;
}

bool ecs_term_next(
    ecs_iter_t *it)
{
    ecs_check(it != NULL, ECS_INVALID_PARAMETER, NULL);
    ecs_check(it->next == ecs_term_next, ECS_INVALID_PARAMETER, NULL);

    flecs_iter_validate(it);

    ecs_term_iter_t *iter = &it->priv.iter.term;
    ecs_term_t *term = &iter->term;
    ecs_world_t *world = it->real_world;
    ecs_table_t *table;

    it->ids = &iter->id;
    it->sources = &iter->subject;
    it->columns = &iter->column;
    it->terms = &iter->term;
    it->sizes = &iter->size;
    it->ptrs = &iter->ptr;

    ecs_iter_t *chain_it = it->chain_it;
    if (chain_it) {
        ecs_iter_next_action_t next = chain_it->next;
        bool match;

        do {
            if (!next(chain_it)) {
                goto done;
            }

            table = chain_it->table;
            match = flecs_term_match_table(world, term, table,
                it->ids, it->columns, it->sources, it->match_indices, true,
                it->flags);
        } while (!match);
        goto yield;

    } else {
        if (!flecs_term_iter_next(world, iter, false, false)) {
            goto done;
        }

        table = iter->table;

        /* Source must either be 0 (EcsThis) or nonzero in case of substitution */
        ecs_assert(iter->subject || iter->cur != iter->set_index, 
            ECS_INTERNAL_ERROR, NULL);
        ecs_assert(iter->table != NULL, ECS_INTERNAL_ERROR, NULL);
    }

yield:
    flecs_iter_populate_data(world, it, table, 0, ecs_table_count(table), 
        it->ptrs);
    ECS_BIT_SET(it->flags, EcsIterIsValid);
    return true;
done:
    ecs_iter_fini(it);
error:
    return false;
}

static
void flecs_init_filter_iter(
    ecs_iter_t *it,
    const ecs_filter_t *filter)
{
    ecs_assert(filter != NULL, ECS_INTERNAL_ERROR, NULL);
    it->priv.iter.filter.filter = filter;
    it->field_count = filter->field_count;
}

int32_t ecs_filter_pivot_term(
    const ecs_world_t *world,
    const ecs_filter_t *filter)
{
    ecs_check(world != NULL, ECS_INVALID_PARAMETER, NULL);
    ecs_check(filter != NULL, ECS_INVALID_PARAMETER, NULL);

    ecs_term_t *terms = filter->terms;
    int32_t i, term_count = filter->term_count;
    int32_t pivot_term = -1, min_count = -1, self_pivot_term = -1;

    for (i = 0; i < term_count; i ++) {
        ecs_term_t *term = &terms[i];
        ecs_id_t id = term->id;

        if ((term->oper != EcsAnd) || (i && (term[-1].oper == EcsOr))) {
            continue;
        }

        if (!ecs_term_match_this(term)) {
            continue;
        }

        ecs_id_record_t *idr = flecs_query_id_record_get(world, id);
        if (!idr) {
            /* If one of the terms does not match with any data, iterator 
             * should not return anything */
            return -2; /* -2 indicates filter doesn't match anything */
        }

        int32_t table_count = flecs_table_cache_count(&idr->cache);
        if (min_count == -1 || table_count < min_count) {
            min_count = table_count;
            pivot_term = i;
            if ((term->src.flags & EcsTraverseFlags) == EcsSelf) {
                self_pivot_term = i;
            }
        }
    }

    if (self_pivot_term != -1) {
        pivot_term = self_pivot_term;
    }

    return pivot_term;
error:
    return -2;
}

void flecs_filter_apply_iter_flags(
    ecs_iter_t *it,
    const ecs_filter_t *filter)
{
    ECS_BIT_COND(it->flags, EcsIterIsInstanced, 
        ECS_BIT_IS_SET(filter->flags, EcsFilterIsInstanced));
    ECS_BIT_COND(it->flags, EcsIterNoData,
        ECS_BIT_IS_SET(filter->flags, EcsFilterNoData));
    ECS_BIT_COND(it->flags, EcsIterHasCondSet, 
        ECS_BIT_IS_SET(filter->flags, EcsFilterHasCondSet));
}

ecs_iter_t flecs_filter_iter_w_flags(
    const ecs_world_t *stage,
    const ecs_filter_t *filter,
    ecs_flags32_t flags)
{
    ecs_check(stage != NULL, ECS_INVALID_PARAMETER, NULL);
    ecs_check(filter != NULL, ECS_INVALID_PARAMETER, NULL);
    ecs_check(!(filter->flags & (EcsFilterHasPred|EcsFilterHasScopes)),
        ECS_UNSUPPORTED, NULL);
    const ecs_world_t *world = ecs_get_world(stage);
    
    if (!(flags & EcsIterMatchVar)) {
        flecs_process_pending_tables(world);
    }

    ecs_iter_t it = {
        .real_world = (ecs_world_t*)world,
        .world = (ecs_world_t*)stage,
        .terms = filter ? filter->terms : NULL,
        .next = ecs_filter_next,
        .flags = flags,
        .sizes = filter->sizes
    };

    ecs_filter_iter_t *iter = &it.priv.iter.filter;
    iter->pivot_term = -1;

    flecs_init_filter_iter(&it, filter);
    flecs_filter_apply_iter_flags(&it, filter);

    /* Find term that represents smallest superset */
    if (ECS_BIT_IS_SET(flags, EcsIterIgnoreThis)) {
        term_iter_init_no_data(&iter->term_iter);
    } else if (ECS_BIT_IS_SET(filter->flags, EcsFilterMatchThis)) {
        ecs_term_t *terms = filter->terms;
        int32_t pivot_term = -1;
        ecs_check(terms != NULL, ECS_INVALID_PARAMETER, NULL);

        pivot_term = ecs_filter_pivot_term(world, filter);
        iter->kind = EcsIterEvalTables;
        iter->pivot_term = pivot_term;

        if (pivot_term == -2) {
            /* One or more terms have no matching results */
            term_iter_init_no_data(&iter->term_iter);
        } else if (pivot_term == -1) {
            /* No terms meet the criteria to be a pivot term, evaluate filter
             * against all tables */
            term_iter_init_wildcard(world, &iter->term_iter, 
                ECS_BIT_IS_SET(filter->flags, EcsFilterMatchEmptyTables));
        } else {
            ecs_assert(pivot_term >= 0, ECS_INTERNAL_ERROR, NULL);
            term_iter_init(world, &terms[pivot_term], &iter->term_iter,
                ECS_BIT_IS_SET(filter->flags, EcsFilterMatchEmptyTables));
        }
    } else {
        if (!ECS_BIT_IS_SET(filter->flags, EcsFilterMatchAnything)) {
            term_iter_init_no_data(&iter->term_iter);
        } else {
            iter->kind = EcsIterEvalNone;
        }
    }

    ECS_BIT_COND(it.flags, EcsIterNoData, 
        ECS_BIT_IS_SET(filter->flags, EcsFilterNoData));

    if (ECS_BIT_IS_SET(filter->flags, EcsFilterMatchThis)) {
        /* Make space for one variable if the filter has terms for This var */ 
        it.variable_count = 1;

        /* Set variable name array */
        it.variable_names = (char**)filter->variable_names;
    }

    flecs_iter_init(stage, &it, flecs_iter_cache_all);

    return it;
error:
    return (ecs_iter_t){ 0 };
}

ecs_iter_t ecs_filter_iter(
    const ecs_world_t *stage,
    const ecs_filter_t *filter)
{
    return flecs_filter_iter_w_flags(stage, filter, 0);
}

ecs_iter_t ecs_filter_chain_iter(
    const ecs_iter_t *chain_it,
    const ecs_filter_t *filter)
{
    ecs_iter_t it = {
        .terms = filter->terms,
        .field_count = filter->field_count,
        .world = chain_it->world,
        .real_world = chain_it->real_world,
        .chain_it = (ecs_iter_t*)chain_it,
        .next = ecs_filter_next,
        .sizes = filter->sizes
    };

    flecs_iter_init(chain_it->world, &it, flecs_iter_cache_all);
    ecs_filter_iter_t *iter = &it.priv.iter.filter;
    flecs_init_filter_iter(&it, filter);

    iter->kind = EcsIterEvalChain;

    return it;
}

bool ecs_filter_next(
    ecs_iter_t *it)
{
    ecs_check(it != NULL, ECS_INVALID_PARAMETER, NULL);
    ecs_check(it->next == ecs_filter_next, ECS_INVALID_PARAMETER, NULL);

    if (flecs_iter_next_row(it)) {
        return true;
    }

    return flecs_iter_next_instanced(it, ecs_filter_next_instanced(it));
error:
    return false;
}

bool ecs_filter_next_instanced(
    ecs_iter_t *it)
{
    ecs_check(it != NULL, ECS_INVALID_PARAMETER, NULL);
    ecs_check(it->next == ecs_filter_next, ECS_INVALID_PARAMETER, NULL);
    ecs_check(it->chain_it != it, ECS_INVALID_PARAMETER, NULL);

    ecs_filter_iter_t *iter = &it->priv.iter.filter;
    const ecs_filter_t *filter = iter->filter;
    ecs_world_t *world = it->real_world;
    ecs_table_t *table = NULL;
    bool match;

    flecs_iter_validate(it);

    ecs_iter_t *chain_it = it->chain_it;
    ecs_iter_kind_t kind = iter->kind;

    if (chain_it) {
        ecs_assert(kind == EcsIterEvalChain, ECS_INVALID_PARAMETER, NULL);
        
        ecs_iter_next_action_t next = chain_it->next;
        do {
            if (!next(chain_it)) {
                ecs_iter_fini(it);
                goto done;
            }

            table = chain_it->table;
            match = flecs_filter_match_table(world, filter, table,
                it->ids, it->columns, it->sources, it->match_indices, NULL, 
                true, -1, it->flags);
        } while (!match);

        goto yield;
    } else if (kind == EcsIterEvalTables || kind == EcsIterEvalCondition) {
        ecs_term_iter_t *term_iter = &iter->term_iter;
        ecs_term_t *term = &term_iter->term;
        int32_t pivot_term = iter->pivot_term;
        bool first;

        /* Check if the This variable has been set on the iterator. If set,
         * the filter should only be applied to the variable value */
        ecs_var_t *this_var = NULL;
        ecs_table_t *this_table = NULL;
        if (it->variable_count) {
            if (ecs_iter_var_is_constrained(it, 0)) {
                this_var = it->variables;
                this_table = this_var->range.table;

                /* If variable is constrained, make sure it's a value that's
                 * pointing to a table, as a filter can't iterate single
                 * entities (yet) */
                ecs_assert(this_table != NULL, ECS_INVALID_OPERATION, NULL);

                /* Can't set variable for filter that does not iterate tables */
                ecs_assert(kind == EcsIterEvalTables, 
                    ECS_INVALID_OPERATION, NULL);
            }
        }

        do {
            /* If there are no matches left for the previous table, this is the
             * first match of the next table. */
            first = iter->matches_left == 0;

            if (first) {
                if (kind != EcsIterEvalCondition) {
                    /* Check if this variable was constrained */
                    if (this_table != NULL) {                        
                        /* If this is the first match of a new result and the
                         * previous result was equal to the value of a 
                         * constrained var, there's nothing left to iterate */
                        if (it->table == this_table) {
                            goto done;
                        }

                        /* If table doesn't match term iterator, it doesn't
                         * match filter. */
                        if (!flecs_term_iter_set_table(
                            world, term_iter, this_table))
                        {
                            goto done;
                        }

                        it->offset = this_var->range.offset;
                        it->count = this_var->range.count;

                        /* But if it does, forward it to filter matching */
                        ecs_assert(term_iter->table == this_table,
                            ECS_INTERNAL_ERROR, NULL);

                    /* If This variable is not constrained, iterate as usual */
                    } else {
                        it->offset = 0;
                        it->count = 0;

                        /* Find new match, starting with the leading term */
                        if (!flecs_term_iter_next(world, term_iter, 
                            ECS_BIT_IS_SET(filter->flags, 
                                EcsFilterMatchPrefab), 
                            ECS_BIT_IS_SET(filter->flags, 
                                EcsFilterMatchDisabled))) 
                        {
                            goto done;
                        }
                    }

                    ecs_assert(term_iter->match_count != 0, 
                        ECS_INTERNAL_ERROR, NULL);

                    if (pivot_term == -1) {
                        /* Without a pivot term, we're iterating all tables with
                         * a wildcard, so the match count is meaningless. */
                        term_iter->match_count = 1;
                    } else {
                        it->match_indices[pivot_term] = term_iter->match_count;
                    }

                    iter->matches_left = term_iter->match_count;

                    /* Filter iterator takes control over iterating all the
                     * permutations that match the wildcard. */
                    term_iter->match_count = 1;

                    table = term_iter->table;

                    if (pivot_term != -1) {
                        int32_t index = term->field_index;
                        it->ids[index] = term_iter->id;
                        it->sources[index] = term_iter->subject;
                        it->columns[index] = term_iter->column;
                    }
                } else {
                    /* Progress iterator to next match for table, if any */
                    table = it->table;
                    if (term_iter->index == 0) {
                        iter->matches_left = 1;
                        term_iter->index = 1; /* prevents looping again */
                    } else {
                        goto done;
                    }
                }

                /* Match the remainder of the terms */
                match = flecs_filter_match_table(world, filter, table,
                    it->ids, it->columns, it->sources,
                    it->match_indices, &iter->matches_left, first, 
                    pivot_term, it->flags);
                if (!match) {
                    it->table = table;
                    iter->matches_left = 0;
                    continue;
                }

                /* Table got matched, set This variable */
                if (table) {
                    ecs_assert(it->variable_count == 1, ECS_INTERNAL_ERROR, NULL);
                    ecs_assert(it->variables != NULL, ECS_INTERNAL_ERROR, NULL);
                    it->variables[0].range.table = table;
                }

                ecs_assert(iter->matches_left != 0, ECS_INTERNAL_ERROR, NULL);
            }

            /* If this is not the first result for the table, and the table
             * is matched more than once, iterate remaining matches */
            if (!first && (iter->matches_left > 0)) {
                table = it->table;

                /* Find first term that still has matches left */
                int32_t i, j, count = it->field_count;
                for (i = count - 1; i >= 0; i --) {
                    int32_t mi = -- it->match_indices[i];
                    if (mi) {
                        if (mi < 0) {
                            continue;
                        }
                        break;
                    }
                }

                /* If matches_left > 0 we should've found at least one match */
                ecs_assert(i >= 0, ECS_INTERNAL_ERROR, NULL);

                /* Progress first term to next match (must be at least one) */
                int32_t column = it->columns[i];
                if (column < 0) {
                    /* If this term was matched on a non-This entity, reconvert
                     * the column back to a positive value */
                    column = -column;
                }

                it->columns[i] = column + 1;
                flecs_term_match_table(world, &filter->terms[i], table, 
                    &it->ids[i], &it->columns[i], &it->sources[i],
                    &it->match_indices[i], false, it->flags);

                /* Reset remaining terms (if any) to first match */
                for (j = i + 1; j < count; j ++) {
                    flecs_term_match_table(world, &filter->terms[j], table, 
                        &it->ids[j], &it->columns[j], &it->sources[j], 
                        &it->match_indices[j], true, it->flags);
                }
            }

            match = iter->matches_left != 0;
            iter->matches_left --;

            ecs_assert(iter->matches_left >= 0, ECS_INTERNAL_ERROR, NULL);
        } while (!match);

        goto yield;
    }

done:
error:
    ecs_iter_fini(it);
    return false;

yield:
    if (!it->count && table) {
        it->count = ecs_table_count(table);
    }
    flecs_iter_populate_data(world, it, table, it->offset, it->count, it->ptrs);
    ECS_BIT_SET(it->flags, EcsIterIsValid);
    return true;    
}

/**
 * @file search.c
 * @brief Search functions to find (component) ids in table types.
 * 
 * Search functions are used to find the column index of a (component) id in a
 * table. Additionally, search functions implement the logic for finding a
 * component id by following a relationship upwards.
 */


static
int32_t flecs_type_search(
    const ecs_table_t *table,
    ecs_id_t search_id,
    ecs_id_record_t *idr,
    ecs_id_t *ids,
    ecs_id_t *id_out,
    ecs_table_record_t **tr_out)
{    
    ecs_table_record_t *tr = ecs_table_cache_get(&idr->cache, table);
    if (tr) {
        int32_t r = tr->column;
        if (tr_out) tr_out[0] = tr;
        if (id_out) {
            if (ECS_PAIR_FIRST(search_id) == EcsUnion) {
                id_out[0] = ids[r];
            } else {
                id_out[0] = flecs_to_public_id(ids[r]);
            }
        }
        return r;
    }

    return -1;
}

static
int32_t flecs_type_offset_search(
    int32_t offset,
    ecs_id_t id,
    ecs_id_t *ids,
    int32_t count,
    ecs_id_t *id_out)
{
    ecs_assert(ids != NULL, ECS_INTERNAL_ERROR, NULL);
    ecs_assert(count > 0, ECS_INTERNAL_ERROR, NULL);
    ecs_assert(offset > 0, ECS_INTERNAL_ERROR, NULL);
    ecs_assert(id != 0, ECS_INVALID_PARAMETER, NULL);

    while (offset < count) {
        ecs_id_t type_id = ids[offset ++];
        if (ecs_id_match(type_id, id)) {
            if (id_out) {
                id_out[0] = flecs_to_public_id(type_id);
            }
            return offset - 1;
        }
    }

    return -1;
}

static
bool flecs_type_can_inherit_id(
    const ecs_world_t *world,
    const ecs_table_t *table,
    const ecs_id_record_t *idr,
    ecs_id_t id)
{
    ecs_assert(idr != NULL, ECS_INTERNAL_ERROR, NULL);
    if (idr->flags & EcsIdDontInherit) {
        return false;
    }
    if (idr->flags & EcsIdExclusive) {
        if (ECS_HAS_ID_FLAG(id, PAIR)) {
            ecs_entity_t er = ECS_PAIR_FIRST(id);
            if (flecs_table_record_get(
                world, table, ecs_pair(er, EcsWildcard))) 
            {
                return false;
            }
        }
    }
    return true;
}

static
int32_t flecs_type_search_relation(
    const ecs_world_t *world,
    const ecs_table_t *table,
    int32_t offset,
    ecs_id_t id,
    ecs_id_record_t *idr,
    ecs_id_t rel,
    ecs_id_record_t *idr_r,
    bool self,
    ecs_entity_t *subject_out,
    ecs_id_t *id_out,
    ecs_table_record_t **tr_out)
{
    ecs_type_t type = table->type;
    ecs_id_t *ids = type.array;
    int32_t count = type.count;

    if (self) {
        if (offset) {
            int32_t r = flecs_type_offset_search(offset, id, ids, count, id_out);
            if (r != -1) {
                return r;
            }
        } else {
            int32_t r = flecs_type_search(table, id, idr, ids, id_out, tr_out);
            if (r != -1) {
                return r;
            }
        }
    }

    ecs_flags32_t flags = table->flags;
    if ((flags & EcsTableHasPairs) && rel) {
        bool is_a = rel == ecs_pair(EcsIsA, EcsWildcard);
        if (is_a) {
            if (!(flags & EcsTableHasIsA)) {
                return -1;
            }
            idr_r = world->idr_isa_wildcard;

            if (!flecs_type_can_inherit_id(world, table, idr, id)) {
                return -1;
            }
        }

        if (!idr_r) {
            idr_r = flecs_id_record_get(world, rel);
            if (!idr_r) {
                return -1;
            }
        }

        ecs_id_t id_r;
        int32_t r, r_column;
        if (offset) {
            r_column = flecs_type_offset_search(offset, rel, ids, count, &id_r);
        } else {
            r_column = flecs_type_search(table, id, idr_r, ids, &id_r, 0);
        }
        while (r_column != -1) {
            ecs_entity_t obj = ECS_PAIR_SECOND(id_r);
            ecs_assert(obj != 0, ECS_INTERNAL_ERROR, NULL);

            ecs_record_t *rec = flecs_entities_get_any(world, obj);
            ecs_assert(rec != NULL, ECS_INTERNAL_ERROR, NULL);

            ecs_table_t *obj_table = rec->table;
            if (obj_table) {
                ecs_assert(obj_table != table, ECS_CYCLE_DETECTED, NULL);
                
                r = flecs_type_search_relation(world, obj_table, 0, id, idr, 
                    rel, idr_r, true, subject_out, id_out, tr_out);
                if (r != -1) {
                    if (subject_out && !subject_out[0]) {
                        subject_out[0] = ecs_get_alive(world, obj);
                    }
                    return r_column;
                }

                if (!is_a) {
                    r = flecs_type_search_relation(world, obj_table, 0, id, idr, 
                        ecs_pair(EcsIsA, EcsWildcard), world->idr_isa_wildcard, 
                            true, subject_out, id_out, tr_out);
                    if (r != -1) {
                        if (subject_out && !subject_out[0]) {
                            subject_out[0] = ecs_get_alive(world, obj);
                        }
                        return r_column;
                    }
                }
            }

            r_column = flecs_type_offset_search(
                r_column + 1, rel, ids, count, &id_r);
        }
    }

    return -1;
}

int32_t flecs_search_relation_w_idr(
    const ecs_world_t *world,
    const ecs_table_t *table,
    int32_t offset,
    ecs_id_t id,
    ecs_entity_t rel,
    ecs_flags32_t flags,
    ecs_entity_t *subject_out,
    ecs_id_t *id_out,
    struct ecs_table_record_t **tr_out,
    ecs_id_record_t *idr)
{
    if (!table) return -1;

    ecs_poly_assert(world, ecs_world_t);
    ecs_assert(id != 0, ECS_INVALID_PARAMETER, NULL);

    flags = flags ? flags : (EcsSelf|EcsUp);

    if (!idr) {
        idr = flecs_query_id_record_get(world, id);
        if (!idr) {
            return -1;
        }
    }

    if (subject_out) subject_out[0] = 0;
    if (!(flags & EcsUp)) {
        if (offset) {
            return ecs_search_offset(world, table, offset, id, id_out);
        } else {
            return flecs_type_search(
                table, id, idr, table->type.array, id_out, tr_out);
        }
    }

    int32_t result = flecs_type_search_relation(world, table, offset, id, idr, 
        ecs_pair(rel, EcsWildcard), NULL, flags & EcsSelf, subject_out, 
            id_out, tr_out);

    return result;
}

int32_t ecs_search_relation(
    const ecs_world_t *world,
    const ecs_table_t *table,
    int32_t offset,
    ecs_id_t id,
    ecs_entity_t rel,
    ecs_flags32_t flags,
    ecs_entity_t *subject_out,
    ecs_id_t *id_out,
    struct ecs_table_record_t **tr_out)
{
    if (!table) return -1;

    ecs_poly_assert(world, ecs_world_t);
    ecs_assert(id != 0, ECS_INVALID_PARAMETER, NULL);

    flags = flags ? flags : (EcsSelf|EcsUp);

    if (subject_out) subject_out[0] = 0;
    if (!(flags & EcsUp)) {
        return ecs_search_offset(world, table, offset, id, id_out);
    }

    ecs_id_record_t *idr = flecs_query_id_record_get(world, id);
    if (!idr) {
        return -1;
    }

    int32_t result = flecs_type_search_relation(world, table, offset, id, idr, 
        ecs_pair(rel, EcsWildcard), NULL, flags & EcsSelf, subject_out, 
            id_out, tr_out);

    return result;
}

int32_t flecs_search_w_idr(
    const ecs_world_t *world,
    const ecs_table_t *table,
    ecs_id_t id,
    ecs_id_t *id_out,
    ecs_id_record_t *idr)
{
    if (!table) return -1;

    ecs_poly_assert(world, ecs_world_t);
    ecs_assert(id != 0, ECS_INVALID_PARAMETER, NULL);
    (void)world;

    ecs_type_t type = table->type;
    ecs_id_t *ids = type.array;
    return flecs_type_search(table, id, idr, ids, id_out, 0);
}

int32_t ecs_search(
    const ecs_world_t *world,
    const ecs_table_t *table,
    ecs_id_t id,
    ecs_id_t *id_out)
{
    if (!table) return -1;

    ecs_poly_assert(world, ecs_world_t);
    ecs_assert(id != 0, ECS_INVALID_PARAMETER, NULL);

    ecs_id_record_t *idr = flecs_query_id_record_get(world, id);
    if (!idr) {
        return -1;
    }

    ecs_type_t type = table->type;
    ecs_id_t *ids = type.array;
    return flecs_type_search(table, id, idr, ids, id_out, 0);
}

int32_t ecs_search_offset(
    const ecs_world_t *world,
    const ecs_table_t *table,
    int32_t offset,
    ecs_id_t id,
    ecs_id_t *id_out)
{
    if (!offset) {
        ecs_poly_assert(world, ecs_world_t);
        return ecs_search(world, table, id, id_out);
    }

    if (!table) return -1;

    ecs_type_t type = table->type;
    ecs_id_t *ids = type.array;
    int32_t count = type.count;
    return flecs_type_offset_search(offset, id, ids, count, id_out);
}

static
int32_t flecs_relation_depth_walk(
    const ecs_world_t *world,
    const ecs_id_record_t *idr,
    const ecs_table_t *first,
    const ecs_table_t *table)
{
    int32_t result = 0;

    const ecs_table_record_t *tr = flecs_id_record_get_table(idr, table);
    if (!tr) {
        return 0;
    }

    int32_t i = tr->column, end = i + tr->count;
    for (; i != end; i ++) {
        ecs_entity_t o = ecs_pair_second(world, table->type.array[i]);
        ecs_assert(o != 0, ECS_INTERNAL_ERROR, NULL);

        ecs_table_t *ot = ecs_get_table(world, o);
        if (!ot) {
            continue;
        }
        
        ecs_assert(ot != first, ECS_CYCLE_DETECTED, NULL);
        int32_t cur = flecs_relation_depth_walk(world, idr, first, ot);
        if (cur > result) {
            result = cur;
        }
    }
    
    return result + 1;
}

int32_t flecs_relation_depth(
    const ecs_world_t *world,
    ecs_entity_t r,
    const ecs_table_t *table)
{
    ecs_id_record_t *idr = flecs_id_record_get(world, ecs_pair(r, EcsWildcard));
    if (!idr) {
        return 0;
    }

    int32_t depth_offset = 0;
    if (table->flags & EcsTableHasTarget) {
        if (ecs_table_get_index(world, table, 
            ecs_pair_t(EcsTarget, r)) != -1)
        {
            ecs_id_t id;
            int32_t col = ecs_search(world, table, 
                ecs_pair(EcsFlatten, EcsWildcard), &id);
            if (col == -1) {
                return 0;
            }

            ecs_entity_t did = ecs_pair_second(world, id);
            ecs_assert(did != 0, ECS_INTERNAL_ERROR, NULL);
            uint64_t *val = ecs_map_get(&world->store.entity_to_depth, did);
            ecs_assert(val != NULL, ECS_INTERNAL_ERROR, NULL);
            depth_offset = flecs_uto(int32_t, val[0]);
        }
    }

    return flecs_relation_depth_walk(world, idr, table, table) + depth_offset;
}

/**
 * @file observer.h
 * @brief Observer implementation.
 * 
 * The observer implementation contains functions for creating, deleting and
 * invoking observers. The code is split up into single-term observers and 
 * multi-term observers. Multi-term observers are created from multiple single-
 * term observers.
 */

#include <stddef.h>

static
ecs_entity_t flecs_get_observer_event(
    ecs_term_t *term,
    ecs_entity_t event)
{
    /* If operator is Not, reverse the event */
    if (term->oper == EcsNot) {
        if (event == EcsOnAdd) {
            event = EcsOnRemove;
        } else if (event == EcsOnRemove) {
            event = EcsOnAdd;
        }
    }

    return event;
}

static
ecs_flags32_t flecs_id_flag_for_event(
    ecs_entity_t e)
{
    if (e == EcsOnAdd) {
        return EcsIdHasOnAdd;
    }
    if (e == EcsOnRemove) {
        return EcsIdHasOnRemove;
    }
    if (e == EcsOnSet) {
        return EcsIdHasOnSet;
    }
    if (e == EcsUnSet) {
        return EcsIdHasUnSet;
    }
    if (e == EcsOnTableFill) {
        return EcsIdHasOnTableFill;
    }
    if (e == EcsOnTableEmpty) {
        return EcsIdHasOnTableEmpty;
    }
    if (e == EcsOnTableCreate) {
        return EcsIdHasOnTableCreate;
    }
    if (e == EcsOnTableDelete) {
        return EcsIdHasOnTableDelete;
    }
    return 0;
}

static
void flecs_inc_observer_count(
    ecs_world_t *world,
    ecs_entity_t event,
    ecs_event_record_t *evt,
    ecs_id_t id,
    int32_t value)
{
    ecs_event_id_record_t *idt = flecs_event_id_record_ensure(world, evt, id);
    ecs_assert(idt != NULL, ECS_INTERNAL_ERROR, NULL);
    
    int32_t result = idt->observer_count += value;
    if (result == 1) {
        /* Notify framework that there are observers for the event/id. This 
         * allows parts of the code to skip event evaluation early */
        flecs_notify_tables(world, id, &(ecs_table_event_t){
            .kind = EcsTableTriggersForId,
            .event = event
        });

        ecs_flags32_t flags = flecs_id_flag_for_event(event);
        if (flags) {
            ecs_id_record_t *idr = flecs_id_record_get(world, id);
            if (idr) {
                idr->flags |= flags;
            }
        }
    } else if (result == 0) {
        /* Ditto, but the reverse */
        flecs_notify_tables(world, id, &(ecs_table_event_t){
            .kind = EcsTableNoTriggersForId,
            .event = event
        });

        ecs_flags32_t flags = flecs_id_flag_for_event(event);
        if (flags) {
            ecs_id_record_t *idr = flecs_id_record_get(world, id);
            if (idr) {
                idr->flags &= ~flags;
            }
        }

        flecs_event_id_record_remove(evt, id);
        ecs_os_free(idt);
    }
}

static
void flecs_register_observer_for_id(
    ecs_world_t *world,
    ecs_observable_t *observable,
    ecs_observer_t *observer,
    size_t offset)
{
    ecs_id_t term_id = observer->register_id;
    ecs_term_t *term = &observer->filter.terms[0];
    ecs_entity_t trav = term->src.trav;

    int i;
    for (i = 0; i < observer->event_count; i ++) {
        ecs_entity_t event = flecs_get_observer_event(
            term, observer->events[i]);

        /* Get observers for event */
        ecs_event_record_t *er = flecs_event_record_ensure(observable, event);
        ecs_assert(er != NULL, ECS_INTERNAL_ERROR, NULL);

        /* Get observers for (component) id for event */
        ecs_event_id_record_t *idt = flecs_event_id_record_ensure(
            world, er, term_id);
        ecs_assert(idt != NULL, ECS_INTERNAL_ERROR, NULL);

        ecs_map_t *observers = ECS_OFFSET(idt, offset);
        ecs_map_init_w_params_if(observers, &world->allocators.ptr);
        ecs_map_insert_ptr(observers, observer->filter.entity, observer);

        flecs_inc_observer_count(world, event, er, term_id, 1);
        if (trav) {
            flecs_inc_observer_count(world, event, er, 
                ecs_pair(trav, EcsWildcard), 1);
        }
    }
}

static
void flecs_uni_observer_register(
    ecs_world_t *world,
    ecs_observable_t *observable,
    ecs_observer_t *observer)
{
    ecs_term_t *term = &observer->filter.terms[0];
    ecs_flags32_t flags = term->src.flags;

    if ((flags & (EcsSelf|EcsUp)) == (EcsSelf|EcsUp)) {
        flecs_register_observer_for_id(world, observable, observer,
            offsetof(ecs_event_id_record_t, self_up));
    } else if (flags & EcsSelf) {
        flecs_register_observer_for_id(world, observable, observer,
            offsetof(ecs_event_id_record_t, self));
    } else if (flags & EcsUp) {
        ecs_assert(term->src.trav != 0, ECS_INTERNAL_ERROR, NULL);
        flecs_register_observer_for_id(world, observable, observer,
            offsetof(ecs_event_id_record_t, up));
    }
}

static
void flecs_unregister_observer_for_id(
    ecs_world_t *world,
    ecs_observable_t *observable,
    ecs_observer_t *observer,
    size_t offset)
{
    ecs_id_t term_id = observer->register_id;
    ecs_term_t *term = &observer->filter.terms[0];
    ecs_entity_t trav = term->src.trav;

    int i;
    for (i = 0; i < observer->event_count; i ++) {
        ecs_entity_t event = flecs_get_observer_event(
            term, observer->events[i]);

        /* Get observers for event */
        ecs_event_record_t *er = flecs_event_record_get(observable, event);
        ecs_assert(er != NULL, ECS_INTERNAL_ERROR, NULL);

        /* Get observers for (component) id */
        ecs_event_id_record_t *idt = flecs_event_id_record_get(er, term_id);
        ecs_assert(idt != NULL, ECS_INTERNAL_ERROR, NULL);

        ecs_map_t *id_observers = ECS_OFFSET(idt, offset);
        ecs_map_remove(id_observers, observer->filter.entity);
        if (!ecs_map_count(id_observers)) {
            ecs_map_fini(id_observers);
        }

        flecs_inc_observer_count(world, event, er, term_id, -1);
        if (trav) {
            flecs_inc_observer_count(world, event, er, 
                ecs_pair(trav, EcsWildcard), -1);
        }
    }
}

static
void flecs_unregister_observer(
    ecs_world_t *world,
    ecs_observable_t *observable,
    ecs_observer_t *observer)
{
    ecs_assert(observer != NULL, ECS_INTERNAL_ERROR, NULL);
    if (!observer->filter.terms) {
        ecs_assert(observer->filter.term_count == 0, ECS_INTERNAL_ERROR, NULL);
        return;
    }

    ecs_term_t *term = &observer->filter.terms[0];
    ecs_flags32_t flags = term->src.flags;

    if ((flags & (EcsSelf|EcsUp)) == (EcsSelf|EcsUp)) {
        flecs_unregister_observer_for_id(world, observable, observer,
            offsetof(ecs_event_id_record_t, self_up));
    } else if (flags & EcsSelf) {
        flecs_unregister_observer_for_id(world, observable, observer,
            offsetof(ecs_event_id_record_t, self));
    } else if (flags & EcsUp) {
        flecs_unregister_observer_for_id(world, observable, observer,
            offsetof(ecs_event_id_record_t, up));
    }
}

static
bool flecs_ignore_observer(
    ecs_observer_t *observer,
    ecs_table_t *table,
    int32_t evtx)
{
    ecs_assert(observer != NULL, ECS_INTERNAL_ERROR, NULL);
    ecs_assert(table != NULL, ECS_INTERNAL_ERROR, NULL);

    int32_t *last_event_id = observer->last_event_id;
    if (last_event_id && last_event_id[0] == evtx) {
        return true;
    }

    ecs_flags32_t table_flags = table->flags, filter_flags = observer->filter.flags;

    bool result = (table_flags & EcsTableIsPrefab) &&
        !(filter_flags & EcsFilterMatchPrefab);
    result = result || ((table_flags & EcsTableIsDisabled) &&
        !(filter_flags & EcsFilterMatchDisabled));

    return result;
}

static
bool flecs_is_simple_result(
    ecs_iter_t *it)
{
    return (it->count == 1) || (it->sizes[0] == 0) || (it->sources[0] == 0);
}

static
void flecs_observer_invoke(
    ecs_world_t *world,
    ecs_iter_t *it,
    ecs_observer_t *observer,
    ecs_iter_action_t callback,
    int32_t term_index,
    bool simple_result) 
{
    ecs_assert(it->callback != NULL, ECS_INVALID_PARAMETER, NULL);

    if (ecs_should_log_3()) {
        char *path = ecs_get_fullpath(world, it->system);
        ecs_dbg_3("observer: invoke %s", path);
        ecs_os_free(path);
    }

    ecs_log_push_3();

    world->info.observers_ran_frame ++;

    ecs_filter_t *filter = &observer->filter;
    ecs_assert(term_index < filter->term_count, ECS_INTERNAL_ERROR, NULL);
    ecs_term_t *term = &filter->terms[term_index];
    if (term->oper != EcsNot) {
        ecs_assert((it->offset + it->count) <= ecs_table_count(it->table), 
            ECS_INTERNAL_ERROR, NULL);
    }

    bool instanced = filter->flags & EcsFilterIsInstanced;
    bool match_this = filter->flags & EcsFilterMatchThis;
    bool table_only = it->flags & EcsIterTableOnly;
    if (match_this && (simple_result || instanced || table_only)) {
        callback(it);
    } else {
        ecs_entity_t observer_src = term->src.id;
        if (observer_src && !(term->src.flags & EcsIsEntity)) {
            observer_src = 0;
        }

        ecs_entity_t *entities = it->entities;
        int32_t i, count = it->count;
        ecs_entity_t src = it->sources[0];
        it->count = 1;
        for (i = 0; i < count; i ++) {
            ecs_entity_t e = entities[i];
            it->entities = &e;
            if (!observer_src) {
                callback(it);
            } else if (observer_src == e) {
                ecs_entity_t dummy = 0;
                it->entities = &dummy;
                if (!src) {
                    it->sources[0] = e;
                }
                callback(it);
                it->sources[0] = src;
                break;
            }
        }
        it->entities = entities;
        it->count = count;
    }

    ecs_log_pop_3();
}

static
void flecs_default_uni_observer_run_callback(ecs_iter_t *it) {
    ecs_observer_t *o = it->ctx;
    it->ctx = o->ctx;
    it->callback = o->callback;

    if (ecs_should_log_3()) {
        char *path = ecs_get_fullpath(it->world, it->system);
        ecs_dbg_3("observer %s", path);
        ecs_os_free(path);
    }

    ecs_log_push_3();
    flecs_observer_invoke(it->real_world, it, o, o->callback, 0,
        flecs_is_simple_result(it));
    ecs_log_pop_3();
}

static
void flecs_uni_observer_invoke(
    ecs_world_t *world,
    ecs_observer_t *observer,
    ecs_iter_t *it,
    ecs_table_t *table,
    ecs_entity_t trav,
    int32_t evtx,
    bool simple_result)
{
    ecs_filter_t *filter = &observer->filter;
    ecs_term_t *term = &filter->terms[0];
    if (flecs_ignore_observer(observer, table, evtx)) {
        return;
    }

    ecs_assert(trav == 0 || it->sources[0] != 0, ECS_INTERNAL_ERROR, NULL);
    if (trav && term->src.trav != trav) {
        return;
    }

    bool is_filter = term->inout == EcsInOutNone;
    ECS_BIT_COND(it->flags, EcsIterNoData, is_filter);
    it->system = observer->filter.entity;
    it->ctx = observer->ctx;
    it->binding_ctx = observer->binding_ctx;
    it->term_index = observer->term_index;
    it->terms = term;

    ecs_entity_t event = it->event;
    it->event = flecs_get_observer_event(term, event);

    if (observer->run) {
        it->next = flecs_default_observer_next_callback;
        it->callback = flecs_default_uni_observer_run_callback;
        it->ctx = observer;
        observer->run(it);
    } else {
        ecs_iter_action_t callback = observer->callback;
        it->callback = callback;
        flecs_observer_invoke(world, it, observer, callback, 0, simple_result);
    }

    it->event = event;
}

void flecs_observers_invoke(
    ecs_world_t *world,
    ecs_map_t *observers,
    ecs_iter_t *it,
    ecs_table_t *table,
    ecs_entity_t trav,
    int32_t evtx)
{
    if (ecs_map_is_init(observers)) {
        ecs_table_lock(it->world, table);

        bool simple_result = flecs_is_simple_result(it);
        ecs_map_iter_t oit = ecs_map_iter(observers);
        while (ecs_map_next(&oit)) {
            ecs_observer_t *o = ecs_map_ptr(&oit);
            ecs_assert(it->table == table, ECS_INTERNAL_ERROR, NULL);
            flecs_uni_observer_invoke(world, o, it, table, trav, evtx, simple_result);
        }

        ecs_table_unlock(it->world, table);
    }
}

static
bool flecs_multi_observer_invoke(ecs_iter_t *it) {
    ecs_observer_t *o = it->ctx;
    ecs_world_t *world = it->real_world;

    if (o->last_event_id[0] == world->event_id) {
        /* Already handled this event */
        return false;
    }

    o->last_event_id[0] = world->event_id;

    ecs_iter_t user_it = *it;
    user_it.field_count = o->filter.field_count;
    user_it.terms = o->filter.terms;
    user_it.flags = 0;
    ECS_BIT_COND(user_it.flags, EcsIterNoData,    
        ECS_BIT_IS_SET(o->filter.flags, EcsFilterNoData));
    user_it.ids = NULL;
    user_it.columns = NULL;
    user_it.sources = NULL;
    user_it.sizes = NULL;
    user_it.ptrs = NULL;

    flecs_iter_init(it->world, &user_it, flecs_iter_cache_all);
    user_it.flags |= (it->flags & EcsIterTableOnly);

    ecs_table_t *table = it->table;
    ecs_table_t *prev_table = it->other_table;
    int32_t pivot_term = it->term_index;
    ecs_term_t *term = &o->filter.terms[pivot_term];

    int32_t column = it->columns[0];
    if (term->oper == EcsNot) {
        table = it->other_table;
        prev_table = it->table;
    }

    if (!table) {
        table = &world->store.root;
    }
    if (!prev_table) {
        prev_table = &world->store.root;
    }

    if (column < 0) {
        column = -column;
    }

    user_it.columns[0] = 0;
    user_it.columns[pivot_term] = column;
    user_it.sources[pivot_term] = it->sources[0];
    user_it.sizes = o->filter.sizes;

    if (flecs_filter_match_table(world, &o->filter, table, user_it.ids, 
        user_it.columns, user_it.sources, NULL, NULL, false, pivot_term, 
        user_it.flags))
    {
        /* Monitor observers only invoke when the filter matches for the first
         * time with an entity */
        if (o->is_monitor) {
            if (flecs_filter_match_table(world, &o->filter, prev_table, 
                NULL, NULL, NULL, NULL, NULL, true, -1, user_it.flags)) 
            {
                goto done;
            }
        }

        /* While filter matching needs to be reversed for a Not term, the
         * component data must be fetched from the table we got notified for.
         * Repeat the matching process for the non-matching table so we get the
         * correct column ids and sources, which we need for populate_data */
        if (term->oper == EcsNot) {
            flecs_filter_match_table(world, &o->filter, prev_table, user_it.ids, 
                user_it.columns, user_it.sources, NULL, NULL, false, -1, 
                user_it.flags | EcsFilterPopulate);
        }

        flecs_iter_populate_data(world, &user_it, it->table, it->offset, 
            it->count, user_it.ptrs);

        user_it.ptrs[pivot_term] = it->ptrs[0];
        user_it.ids[pivot_term] = it->event_id;
        user_it.system = o->filter.entity;
        user_it.term_index = pivot_term;
        user_it.ctx = o->ctx;
        user_it.binding_ctx = o->binding_ctx;
        user_it.field_count = o->filter.field_count;
        user_it.callback = o->callback;
        
        flecs_iter_validate(&user_it);
        ecs_table_lock(it->world, table);
        flecs_observer_invoke(world, &user_it, o, o->callback, 
            pivot_term, flecs_is_simple_result(&user_it));
        ecs_table_unlock(it->world, table);
        ecs_iter_fini(&user_it);
        return true;
    }

done:
    ecs_iter_fini(&user_it);
    return false;
}

bool ecs_observer_default_run_action(ecs_iter_t *it) {
    ecs_observer_t *o = it->ctx;
    if (o->is_multi) {
        return flecs_multi_observer_invoke(it);
    } else {
        it->ctx = o->ctx;
        ecs_table_lock(it->world, it->table);
        flecs_observer_invoke(it->real_world, it, o, o->callback, 0,
            flecs_is_simple_result(it));
        ecs_table_unlock(it->world, it->table);
        return true;
    }
}

static
void flecs_default_multi_observer_run_callback(ecs_iter_t *it) {
    flecs_multi_observer_invoke(it);
}

/* For convenience, so applications can (in theory) use a single run callback 
 * that uses ecs_iter_next to iterate results */
bool flecs_default_observer_next_callback(ecs_iter_t *it) {
    if (it->interrupted_by) {
        return false;
    } else {
        /* Use interrupted_by to signal the next iteration must return false */
        it->interrupted_by = it->system;
        return true;
    }
}

/* Run action for children of multi observer */
static
void flecs_multi_observer_builtin_run(ecs_iter_t *it) {
    ecs_observer_t *observer = it->ctx;
    ecs_run_action_t run = observer->run;

    if (run) {
        it->next = flecs_default_observer_next_callback;
        it->callback = flecs_default_multi_observer_run_callback;
        it->interrupted_by = 0;
        run(it);
    } else {
        flecs_multi_observer_invoke(it);
    }
}

static
void flecs_uni_observer_trigger_existing(
    ecs_world_t *world,
    ecs_observer_t *observer)
{
    ecs_iter_action_t callback = observer->callback;

    /* If yield existing is enabled, observer for each thing that matches
     * the event, if the event is iterable. */
    int i, count = observer->event_count;
    for (i = 0; i < count; i ++) {
        ecs_entity_t evt = observer->events[i];
        const EcsIterable *iterable = ecs_get(world, evt, EcsIterable);
        if (!iterable) {
            continue;
        }

        ecs_iter_t it;
        iterable->init(world, world, &it, &observer->filter.terms[0]);
        it.system = observer->filter.entity;
        it.ctx = observer->ctx;
        it.binding_ctx = observer->binding_ctx;
        it.event = evt;

        ecs_iter_next_action_t next = it.next;
        ecs_assert(next != NULL, ECS_INTERNAL_ERROR, NULL);
        while (next(&it)) {
            it.event_id = it.ids[0];
            callback(&it);
        }

        ecs_iter_fini(&it);
    }
}

static
void flecs_multi_observer_yield_existing(
    ecs_world_t *world,
    ecs_observer_t *observer)
{
    ecs_run_action_t run = observer->run;
    if (!run) {
        run = flecs_default_multi_observer_run_callback;
    }

    ecs_run_aperiodic(world, EcsAperiodicEmptyTables);

    int32_t pivot_term = ecs_filter_pivot_term(world, &observer->filter);
    if (pivot_term < 0) {
        return;
    }

    /* If yield existing is enabled, invoke for each thing that matches
     * the event, if the event is iterable. */
    int i, count = observer->event_count;
    for (i = 0; i < count; i ++) {
        ecs_entity_t evt = observer->events[i];
        const EcsIterable *iterable = ecs_get(world, evt, EcsIterable);
        if (!iterable) {
            continue;
        }

        ecs_iter_t it;
        iterable->init(world, world, &it, &observer->filter.terms[pivot_term]);
        it.terms = observer->filter.terms;
        it.field_count = 1;
        it.term_index = pivot_term;
        it.system = observer->filter.entity;
        it.ctx = observer;
        it.binding_ctx = observer->binding_ctx;
        it.event = evt;

        ecs_iter_next_action_t next = it.next;
        ecs_assert(next != NULL, ECS_INTERNAL_ERROR, NULL);
        while (next(&it)) {
            it.event_id = it.ids[0];
            run(&it);
            world->event_id ++;
        }
    }
}

static
int flecs_uni_observer_init(
    ecs_world_t *world,
    ecs_observer_t *observer,
    const ecs_observer_desc_t *desc)
{
    ecs_term_t *term = &observer->filter.terms[0];
    observer->last_event_id = desc->last_event_id;    
    if (!observer->last_event_id) {
        observer->last_event_id = &observer->last_event_id_storage;
    }
    observer->register_id = flecs_from_public_id(world, term->id);
    term->field_index = desc->term_index;

    if (ecs_id_is_tag(world, term->id)) {
        /* If id is a tag, downgrade OnSet/UnSet to OnAdd/OnRemove. */
        int32_t e, count = observer->event_count;
        for (e = 0; e < count; e ++) {
            if (observer->events[e] == EcsOnSet) {
                observer->events[e] = EcsOnAdd;
            } else
            if (observer->events[e] == EcsUnSet) {
                observer->events[e] = EcsOnRemove;
            }
        }
    }

    flecs_uni_observer_register(world, observer->observable, observer);

    if (desc->yield_existing) {
        flecs_uni_observer_trigger_existing(world, observer);
    }

    return 0;
}

static
int flecs_multi_observer_init(
    ecs_world_t *world,
    ecs_observer_t *observer,
    const ecs_observer_desc_t *desc)
{
    /* Create last event id for filtering out the same event that arrives from
     * more than one term */
    observer->last_event_id = ecs_os_calloc_t(int32_t);
    
    /* Mark observer as multi observer */
    observer->is_multi = true;

    /* Create a child observer for each term in the filter */
    ecs_filter_t *filter = &observer->filter;
    ecs_observer_desc_t child_desc = *desc;
    child_desc.last_event_id = observer->last_event_id;
    child_desc.run = NULL;
    child_desc.callback = flecs_multi_observer_builtin_run;
    child_desc.ctx = observer;
    child_desc.ctx_free = NULL;
    child_desc.filter.expr = NULL;
    child_desc.filter.terms_buffer = NULL;
    child_desc.filter.terms_buffer_count = 0;
    child_desc.binding_ctx = NULL;
    child_desc.binding_ctx_free = NULL;
    child_desc.yield_existing = false;
    ecs_os_zeromem(&child_desc.entity);
    ecs_os_zeromem(&child_desc.filter.terms);
    ecs_os_memcpy_n(child_desc.events, observer->events, 
        ecs_entity_t, observer->event_count);

    int i, term_count = filter->term_count;
    bool optional_only = filter->flags & EcsFilterMatchThis;
    for (i = 0; i < term_count; i ++) {
        if (filter->terms[i].oper != EcsOptional) {
            if (ecs_term_match_this(&filter->terms[i])) {
                optional_only = false;
            }
        }
    }

    if (filter->flags & EcsFilterMatchPrefab) {
        child_desc.filter.flags |= EcsFilterMatchPrefab;
    }
    if (filter->flags & EcsFilterMatchDisabled) {
        child_desc.filter.flags |= EcsFilterMatchDisabled;
    }

    /* Create observers as children of observer */
    ecs_entity_t old_scope = ecs_set_scope(world, observer->filter.entity);

    for (i = 0; i < term_count; i ++) {
        if (filter->terms[i].src.flags & EcsFilter) {
            continue;
        }

        ecs_term_t *term = &child_desc.filter.terms[0];
        child_desc.term_index = filter->terms[i].field_index;
        *term = filter->terms[i];

        ecs_oper_kind_t oper = term->oper;
        ecs_id_t id = term->id;

        /* AndFrom & OrFrom terms insert multiple observers */
        if (oper == EcsAndFrom || oper == EcsOrFrom) {
            const ecs_type_t *type = ecs_get_type(world, id);
            int32_t ti, ti_count = type->count;
            ecs_id_t *ti_ids = type->array;

            /* Correct operator will be applied when an event occurs, and
             * the observer is evaluated on the observer source */
            term->oper = EcsAnd;
            for (ti = 0; ti < ti_count; ti ++) {
                ecs_id_t ti_id = ti_ids[ti];
                ecs_id_record_t *idr = flecs_id_record_get(world, ti_id);
                if (idr->flags & EcsIdDontInherit) {
                    continue;
                }

                term->first.name = NULL;
                term->first.id = ti_ids[ti];
                term->id = ti_ids[ti];

                if (ecs_observer_init(world, &child_desc) == 0) {
                    goto error;
                }
            }
            continue;
        }

        /* Single component observers never use OR */
        if (oper == EcsOr) {
            term->oper = EcsAnd;
        }

        /* If observer only contains optional terms, match everything */
        if (optional_only) {
            term->id = EcsAny;
            term->first.id = EcsAny;
            term->src.id = EcsThis;
            term->src.flags = EcsIsVariable;
            term->second.id = 0;
        } else if (term->oper == EcsOptional) {
            continue;
        }
        if (ecs_observer_init(world, &child_desc) == 0) {
            goto error;
        }

        if (optional_only) {
            break;
        }
    }

    ecs_set_scope(world, old_scope);

    if (desc->yield_existing) {
        flecs_multi_observer_yield_existing(world, observer);
    }

    return 0; 
error:
    return -1;
}

ecs_entity_t ecs_observer_init(
    ecs_world_t *world,
    const ecs_observer_desc_t *desc)
{
    ecs_check(world != NULL, ECS_INVALID_PARAMETER, NULL);
    ecs_check(desc != NULL, ECS_INVALID_PARAMETER, NULL);
    ecs_check(desc->_canary == 0, ECS_INVALID_PARAMETER, NULL);
    ecs_check(!(world->flags & EcsWorldFini), ECS_INVALID_OPERATION, NULL);

    ecs_entity_t entity = desc->entity;
    if (!entity) {
        entity = ecs_new(world, 0);
    }
    EcsPoly *poly = ecs_poly_bind(world, entity, ecs_observer_t);
    if (!poly->poly) {
        ecs_check(desc->callback != NULL || desc->run != NULL, 
            ECS_INVALID_OPERATION, NULL);

        ecs_observer_t *observer = ecs_poly_new(ecs_observer_t);
        ecs_assert(observer != NULL, ECS_INTERNAL_ERROR, NULL);
        observer->dtor = (ecs_poly_dtor_t)flecs_observer_fini;

        /* Make writeable copy of filter desc so that we can set name. This will
         * make debugging easier, as any error messages related to creating the
         * filter will have the name of the observer. */
        ecs_filter_desc_t filter_desc = desc->filter;
        filter_desc.entity = entity;
        ecs_filter_t *filter = filter_desc.storage = &observer->filter;
        *filter = ECS_FILTER_INIT;

        /* Parse filter */
        if (ecs_filter_init(world, &filter_desc) == NULL) {
            flecs_observer_fini(observer);
            return 0;
        }

        /* Observer must have at least one term */
        ecs_check(observer->filter.term_count > 0, ECS_INVALID_PARAMETER, NULL);

        poly->poly = observer;

        ecs_observable_t *observable = desc->observable;
        if (!observable) {
            observable = ecs_get_observable(world);
        }

        observer->run = desc->run;
        observer->callback = desc->callback;
        observer->ctx = desc->ctx;
        observer->binding_ctx = desc->binding_ctx;
        observer->ctx_free = desc->ctx_free;
        observer->binding_ctx_free = desc->binding_ctx_free;
        observer->term_index = desc->term_index;
        observer->observable = observable;

        /* Check if observer is monitor. Monitors are created as multi observers
         * since they require pre/post checking of the filter to test if the
         * entity is entering/leaving the monitor. */
        int i;
        for (i = 0; i < FLECS_EVENT_DESC_MAX; i ++) {
            ecs_entity_t event = desc->events[i];
            if (!event) {
                break;
            }

            if (event == EcsMonitor) {
                /* Monitor event must be first and last event */
                ecs_check(i == 0, ECS_INVALID_PARAMETER, NULL);

                observer->events[0] = EcsOnAdd;
                observer->events[1] = EcsOnRemove;
                observer->event_count ++;
                observer->is_monitor = true;
            } else {
                observer->events[i] = event;
            }

            observer->event_count ++;
        }

        /* Observer must have at least one event */
        ecs_check(observer->event_count != 0, ECS_INVALID_PARAMETER, NULL);

        bool multi = false;

        if (filter->term_count == 1 && !desc->last_event_id) {
            ecs_term_t *term = &filter->terms[0];
            /* If the filter has a single term but it is a *From operator, we
             * need to create a multi observer */
            multi |= (term->oper == EcsAndFrom) || (term->oper == EcsOrFrom);
            
            /* An observer with only optional terms is a special case that is
             * only handled by multi observers */
            multi |= term->oper == EcsOptional;
        }

        if (filter->term_count == 1 && !observer->is_monitor && !multi) {
            if (flecs_uni_observer_init(world, observer, desc)) {
                goto error;
            }
        } else {
            if (flecs_multi_observer_init(world, observer, desc)) {
                goto error;
            }
        }

        if (ecs_get_name(world, entity)) {
            ecs_trace("#[green]observer#[reset] %s created", 
                ecs_get_name(world, entity));
        }
    } else {
        ecs_observer_t *observer = ecs_poly(poly->poly, ecs_observer_t);

        if (desc->run) {
            observer->run = desc->run;
        }
        if (desc->callback) {
            observer->callback = desc->callback;
        }

        if (observer->ctx_free) {
            if (observer->ctx && observer->ctx != desc->ctx) {
                observer->ctx_free(observer->ctx);
            }
        }
        if (observer->binding_ctx_free) {
            if (observer->binding_ctx && observer->binding_ctx != desc->binding_ctx) {
                observer->binding_ctx_free(observer->binding_ctx);
            }
        }

        if (desc->ctx) {
            observer->ctx = desc->ctx;
        }
        if (desc->binding_ctx) {
            observer->binding_ctx = desc->binding_ctx;
        }
        if (desc->ctx_free) {
            observer->ctx_free = desc->ctx_free;
        }
        if (desc->binding_ctx_free) {
            observer->binding_ctx_free = desc->binding_ctx_free;
        }
    }

    ecs_poly_modified(world, entity, ecs_observer_t);

    return entity;
error:
    ecs_delete(world, entity);
    return 0;
}

void* ecs_get_observer_ctx(
    const ecs_world_t *world,
    ecs_entity_t observer)
{
    const EcsPoly *o = ecs_poly_bind_get(world, observer, ecs_observer_t);
    if (o) {
        ecs_poly_assert(o->poly, ecs_observer_t);
        return ((ecs_observer_t*)o->poly)->ctx;
    } else {
        return NULL;
    }     
}

void* ecs_get_observer_binding_ctx(
    const ecs_world_t *world,
    ecs_entity_t observer)
{
    const EcsPoly *o = ecs_poly_bind_get(world, observer, ecs_observer_t);
    if (o) {
        ecs_poly_assert(o->poly, ecs_observer_t);
        return ((ecs_observer_t*)o->poly)->binding_ctx;
    } else {
        return NULL;
    }      
}

void flecs_observer_fini(
    ecs_observer_t *observer)
{
    if (observer->is_multi) {
        ecs_os_free(observer->last_event_id);
    } else {
        if (observer->filter.term_count) {
            flecs_unregister_observer(
                observer->filter.world, observer->observable, observer);
        } else {
            /* Observer creation failed while creating filter */
        }
    }

    /* Cleanup filters */
    ecs_filter_fini(&observer->filter);

    /* Cleanup context */
    if (observer->ctx_free) {
        observer->ctx_free(observer->ctx);
    }

    if (observer->binding_ctx_free) {
        observer->binding_ctx_free(observer->binding_ctx);
    }

    ecs_poly_free(observer, ecs_observer_t);
}

/**
 * @file table_cache.c
 * @brief Data structure for fast table iteration/lookups.
 * 
 * A table cache is a data structure that provides constant time operations for
 * insertion and removal of tables, and to testing whether a table is registered
 * with the cache. A table cache also provides functions to iterate the tables
 * in a cache.
 * 
 * The world stores a table cache per (component) id inside the id record 
 * administration. Cached queries store a table cache with matched tables.
 * 
 * A table cache has separate lists for non-empty tables and empty tables. This
 * improves performance as applications don't waste time iterating empty tables.
 */


static
void flecs_table_cache_list_remove(
    ecs_table_cache_t *cache,
    ecs_table_cache_hdr_t *elem)
{
    ecs_table_cache_hdr_t *next = elem->next;
    ecs_table_cache_hdr_t *prev = elem->prev;

    if (next) {
        next->prev = prev;
    }
    if (prev) {
        prev->next = next;
    }

    cache->empty_tables.count -= !!elem->empty;
    cache->tables.count -= !elem->empty;

    if (cache->empty_tables.first == elem) {
        cache->empty_tables.first = next;
    } else if (cache->tables.first == elem) {
        cache->tables.first = next;
    }
    if (cache->empty_tables.last == elem) {
        cache->empty_tables.last = prev;
    }
    if (cache->tables.last == elem) {
        cache->tables.last = prev;
    }
}

static
void flecs_table_cache_list_insert(
    ecs_table_cache_t *cache,
    ecs_table_cache_hdr_t *elem)
{
    ecs_table_cache_hdr_t *last;
    if (elem->empty) {
        last = cache->empty_tables.last;
        cache->empty_tables.last = elem;
        if ((++ cache->empty_tables.count) == 1) {
            cache->empty_tables.first = elem;
        }
    } else {
        last = cache->tables.last;
        cache->tables.last = elem;
        if ((++ cache->tables.count) == 1) {
            cache->tables.first = elem;
        }
    }

    elem->next = NULL;
    elem->prev = last;

    if (last) {
        last->next = elem;
    }
}

void ecs_table_cache_init(
    ecs_world_t *world,
    ecs_table_cache_t *cache)
{
    ecs_assert(cache != NULL, ECS_INTERNAL_ERROR, NULL);
    ecs_map_init_w_params(&cache->index, &world->allocators.ptr);
}

void ecs_table_cache_fini(
    ecs_table_cache_t *cache)
{
    ecs_assert(cache != NULL, ECS_INTERNAL_ERROR, NULL);
    ecs_map_fini(&cache->index);
}

bool ecs_table_cache_is_empty(
    const ecs_table_cache_t *cache)
{
    return ecs_map_count(&cache->index) == 0;
}

void ecs_table_cache_insert(
    ecs_table_cache_t *cache,
    const ecs_table_t *table,
    ecs_table_cache_hdr_t *result)
{
    ecs_assert(cache != NULL, ECS_INTERNAL_ERROR, NULL);
    ecs_assert(ecs_table_cache_get(cache, table) == NULL,
        ECS_INTERNAL_ERROR, NULL);
    ecs_assert(result != NULL, ECS_INTERNAL_ERROR, NULL);

    bool empty;
    if (!table) {
        empty = false;
    } else {
        empty = ecs_table_count(table) == 0;
    }

    result->cache = cache;
    result->table = (ecs_table_t*)table;
    result->empty = empty;

    flecs_table_cache_list_insert(cache, result);

    if (table) {
        ecs_map_insert_ptr(&cache->index, table->id, result);
    }

    ecs_assert(empty || cache->tables.first != NULL, 
        ECS_INTERNAL_ERROR, NULL);
    ecs_assert(!empty || cache->empty_tables.first != NULL, 
        ECS_INTERNAL_ERROR, NULL);
}

void ecs_table_cache_replace(
    ecs_table_cache_t *cache,
    const ecs_table_t *table,
    ecs_table_cache_hdr_t *elem)
{
    ecs_table_cache_hdr_t **r = ecs_map_get_ref(
        &cache->index, ecs_table_cache_hdr_t, table->id);
    ecs_assert(r != NULL, ECS_INTERNAL_ERROR, NULL);

    ecs_table_cache_hdr_t *old = *r;
    ecs_assert(old != NULL, ECS_INTERNAL_ERROR, NULL);

    ecs_table_cache_hdr_t *prev = old->prev, *next = old->next;
    if (prev) {
        ecs_assert(prev->next == old, ECS_INTERNAL_ERROR, NULL);
        prev->next = elem;
    }
    if (next) {
        ecs_assert(next->prev == old, ECS_INTERNAL_ERROR, NULL);
        next->prev = elem;
    }

    if (cache->empty_tables.first == old) {
        cache->empty_tables.first = elem;
    }
    if (cache->empty_tables.last == old) {
        cache->empty_tables.last = elem;
    }
    if (cache->tables.first == old) {
        cache->tables.first = elem;
    }
    if (cache->tables.last == old) {
        cache->tables.last = elem;
    }

    *r = elem;
    elem->prev = prev;
    elem->next = next;
}

void* ecs_table_cache_get(
    const ecs_table_cache_t *cache,
    const ecs_table_t *table)
{
    ecs_assert(cache != NULL, ECS_INTERNAL_ERROR, NULL);
    if (table) {
        if (ecs_map_is_init(&cache->index)) {
            return ecs_map_get_deref(&cache->index, void**, table->id);
        }
        return NULL;
    } else {
        ecs_table_cache_hdr_t *elem = cache->tables.first;
        ecs_assert(!elem || elem->table == NULL, ECS_INTERNAL_ERROR, NULL);
        return elem;
    }
}

void* ecs_table_cache_remove(
    ecs_table_cache_t *cache,
    uint64_t table_id,
    ecs_table_cache_hdr_t *elem)
{
    ecs_assert(cache != NULL, ECS_INTERNAL_ERROR, NULL);
    ecs_assert(table_id != 0, ECS_INTERNAL_ERROR, NULL);
    ecs_assert(elem != NULL, ECS_INTERNAL_ERROR, NULL);

    ecs_assert(elem->cache == cache, ECS_INTERNAL_ERROR, NULL);

    flecs_table_cache_list_remove(cache, elem);
    ecs_map_remove(&cache->index, table_id);

    return elem;
}

bool ecs_table_cache_set_empty(
    ecs_table_cache_t *cache,
    const ecs_table_t *table,
    bool empty)
{
    ecs_assert(cache != NULL, ECS_INTERNAL_ERROR, NULL);
    ecs_assert(table != NULL, ECS_INTERNAL_ERROR, NULL);

    ecs_table_cache_hdr_t *elem = ecs_map_get_deref(&cache->index, 
        ecs_table_cache_hdr_t, table->id);
    if (!elem) {
        return false;
    }

    if (elem->empty == empty) {
        return false;
    }

    flecs_table_cache_list_remove(cache, elem);
    elem->empty = empty;
    flecs_table_cache_list_insert(cache, elem);

    return true;
}

bool flecs_table_cache_iter(
    ecs_table_cache_t *cache,
    ecs_table_cache_iter_t *out)
{
    ecs_assert(cache != NULL, ECS_INTERNAL_ERROR, NULL);
    ecs_assert(out != NULL, ECS_INTERNAL_ERROR, NULL);
    out->next = cache->tables.first;
    out->next_list = NULL;
    out->cur = NULL;
    return out->next != NULL;
}

bool flecs_table_cache_empty_iter(
    ecs_table_cache_t *cache,
    ecs_table_cache_iter_t *out)
{
    ecs_assert(cache != NULL, ECS_INTERNAL_ERROR, NULL);
    ecs_assert(out != NULL, ECS_INTERNAL_ERROR, NULL);
    out->next = cache->empty_tables.first;
    out->next_list = NULL;
    out->cur = NULL;
    return out->next != NULL;
}

bool flecs_table_cache_all_iter(
    ecs_table_cache_t *cache,
    ecs_table_cache_iter_t *out)
{
    ecs_assert(cache != NULL, ECS_INTERNAL_ERROR, NULL);
    ecs_assert(out != NULL, ECS_INTERNAL_ERROR, NULL);
    out->next = cache->empty_tables.first;
    out->next_list = cache->tables.first;
    out->cur = NULL;
    return out->next != NULL || out->next_list != NULL;
}

ecs_table_cache_hdr_t* _flecs_table_cache_next(
    ecs_table_cache_iter_t *it)
{
    ecs_table_cache_hdr_t *next = it->next;
    if (!next) {
        next = it->next_list;
        it->next_list = NULL;
        if (!next) {
            return false;
        }
    }

    it->cur = next;
    it->next = next->next;
    return next;
}

/**
 * @file os_api.h
 * @brief Operating system abstraction API.
 * 
 * The OS API implements an overridable interface for implementing functions 
 * that are operating system specific, in addition to a number of hooks which
 * allow for customization by the user, like logging.
 */

#include <ctype.h>
#include <time.h>

void ecs_os_api_impl(ecs_os_api_t *api);

static bool ecs_os_api_initialized = false;
static bool ecs_os_api_initializing = false;
static int ecs_os_api_init_count = 0;

#ifndef __EMSCRIPTEN__
ecs_os_api_t ecs_os_api = {
    .flags_ = EcsOsApiHighResolutionTimer | EcsOsApiLogWithColors,
    .log_level_ = -1 /* Disable tracing by default, but log warnings/errors */
};
#else
/* Disable colors by default for emscripten */
ecs_os_api_t ecs_os_api = {
    .flags_ = EcsOsApiHighResolutionTimer,
    .log_level_ = -1 /* Disable tracing by default, but log warnings/errors */
};
#endif

int64_t ecs_os_api_malloc_count = 0;
int64_t ecs_os_api_realloc_count = 0;
int64_t ecs_os_api_calloc_count = 0;
int64_t ecs_os_api_free_count = 0;

void ecs_os_set_api(
    ecs_os_api_t *os_api)
{
    if (!ecs_os_api_initialized) {
        ecs_os_api = *os_api;
        ecs_os_api_initialized = true;
    }
}

ecs_os_api_t ecs_os_get_api(void) {
    return ecs_os_api;
}

void ecs_os_init(void)
{
    if (!ecs_os_api_initialized) {
        ecs_os_set_api_defaults();
    }
    
    if (!(ecs_os_api_init_count ++)) {
        if (ecs_os_api.init_) {
            ecs_os_api.init_();
        }
    }
}

void ecs_os_fini(void) {
    if (!--ecs_os_api_init_count) {
        if (ecs_os_api.fini_) {
            ecs_os_api.fini_();
        }
    }
}

/* Assume every non-glibc Linux target has no execinfo.
   This mainly fixes musl support, as musl doesn't define any preprocessor macro specifying its presence. */ 
#if defined(ECS_TARGET_LINUX) && !defined(__GLIBC__)
#define HAVE_EXECINFO 0
#elif !defined(ECS_TARGET_WINDOWS) && !defined(ECS_TARGET_EM) && !defined(ECS_TARGET_ANDROID)
#define HAVE_EXECINFO 1
#else
#define HAVE_EXECINFO 0
#endif

#if HAVE_EXECINFO
#include <execinfo.h>
#define ECS_BT_BUF_SIZE 100

void flecs_dump_backtrace(
    FILE *stream) 
{
    int nptrs;
    void *buffer[ECS_BT_BUF_SIZE];
    char **strings;

    nptrs = backtrace(buffer, ECS_BT_BUF_SIZE);

    strings = backtrace_symbols(buffer, nptrs);
    if (strings == NULL) {
        return;
    }

    for (int j = 1; j < nptrs; j++) {
        fprintf(stream, "%s\n", strings[j]);
    }

    free(strings);
}
#else
void flecs_dump_backtrace(
    FILE *stream)
{ 
    (void)stream;
}
#endif
#undef HAVE_EXECINFO_H

static
void flecs_log_msg(
    int32_t level,
    const char *file, 
    int32_t line,  
    const char *msg)
{
    FILE *stream;
    if (level >= 0) {
        stream = stdout;
    } else {
        stream = stderr;
    }

    bool use_colors = ecs_os_api.flags_ & EcsOsApiLogWithColors;
    bool timestamp = ecs_os_api.flags_ & EcsOsApiLogWithTimeStamp;
    bool deltatime = ecs_os_api.flags_ & EcsOsApiLogWithTimeDelta;

    time_t now = 0;

    if (deltatime) {
        now = time(NULL);
        time_t delta = 0;
        if (ecs_os_api.log_last_timestamp_) {
            delta = now - ecs_os_api.log_last_timestamp_;
        }
        ecs_os_api.log_last_timestamp_ = (int64_t)now;

        if (delta) {
            if (delta < 10) {
                fputs(" ", stream);
            }
            if (delta < 100) {
                fputs(" ", stream);
            }
            char time_buf[20];
            ecs_os_sprintf(time_buf, "%u", (uint32_t)delta);
            fputs("+", stream);
            fputs(time_buf, stream);
            fputs(" ", stream);
        } else {
            fputs("     ", stream);
        }
    }

    if (timestamp) {
        if (!now) {
            now = time(NULL);
        }
        char time_buf[20];
        ecs_os_sprintf(time_buf, "%u", (uint32_t)now);
        fputs(time_buf, stream);
        fputs(" ", stream);
    }

    if (level >= 4) {
        if (use_colors) fputs(ECS_NORMAL, stream);
        fputs("jrnl", stream);
    } else if (level >= 0) {
        if (level == 0) {
            if (use_colors) fputs(ECS_MAGENTA, stream);
        } else {
            if (use_colors) fputs(ECS_GREY, stream);
        }
        fputs("info", stream);
    } else if (level == -2) {
        if (use_colors) fputs(ECS_YELLOW, stream);
        fputs("warning", stream);
    } else if (level == -3) {
        if (use_colors) fputs(ECS_RED, stream);
        fputs("error", stream);
    } else if (level == -4) {
        if (use_colors) fputs(ECS_RED, stream);
        fputs("fatal", stream);
    }

    if (use_colors) fputs(ECS_NORMAL, stream);
    fputs(": ", stream);

    if (level >= 0) {
        if (ecs_os_api.log_indent_) {
            char indent[32];
            int i, indent_count = ecs_os_api.log_indent_;
            if (indent_count > 15) indent_count = 15;

            for (i = 0; i < indent_count; i ++) {
                indent[i * 2] = '|';
                indent[i * 2 + 1] = ' ';
            }

            if (ecs_os_api.log_indent_ != indent_count) {
                indent[i * 2 - 2] = '+';
            }

            indent[i * 2] = '\0';

            fputs(indent, stream);
        }
    }

    if (level < 0) {
        if (file) {
            const char *file_ptr = strrchr(file, '/');
            if (!file_ptr) {
                file_ptr = strrchr(file, '\\');
            }

            if (file_ptr) {
                file = file_ptr + 1;
            }

            fputs(file, stream);
            fputs(": ", stream);
        }

        if (line) {
            fprintf(stream, "%d: ", line);
        }
    }

    fputs(msg, stream);

    fputs("\n", stream);

    if (level == -4) {
        flecs_dump_backtrace(stream);
    }
}

void ecs_os_dbg(
    const char *file, 
    int32_t line, 
    const char *msg)
{
    if (ecs_os_api.log_) {
        ecs_os_api.log_(1, file, line, msg);
    }
}

void ecs_os_trace(
    const char *file, 
    int32_t line, 
    const char *msg) 
{
    if (ecs_os_api.log_) {
        ecs_os_api.log_(0, file, line, msg);
    }
}

void ecs_os_warn(
    const char *file, 
    int32_t line, 
    const char *msg) 
{
    if (ecs_os_api.log_) {
        ecs_os_api.log_(-2, file, line, msg);
    }
}

void ecs_os_err(
    const char *file, 
    int32_t line, 
    const char *msg) 
{
    if (ecs_os_api.log_) {
        ecs_os_api.log_(-3, file, line, msg);
    }
}

void ecs_os_fatal(
    const char *file, 
    int32_t line, 
    const char *msg) 
{
    if (ecs_os_api.log_) {
        ecs_os_api.log_(-4, file, line, msg);
    }
}

static
void ecs_os_gettime(ecs_time_t *time) {
    ecs_assert(ecs_os_has_time() == true, ECS_MISSING_OS_API, NULL);
    
    uint64_t now = ecs_os_now();
    uint64_t sec = now / 1000000000;

    assert(sec < UINT32_MAX);
    assert((now - sec * 1000000000) < UINT32_MAX);

    time->sec = (uint32_t)sec;
    time->nanosec = (uint32_t)(now - sec * 1000000000);
}

static
void* ecs_os_api_malloc(ecs_size_t size) {
    ecs_os_linc(&ecs_os_api_malloc_count);
    ecs_assert(size > 0, ECS_INVALID_PARAMETER, NULL);
    return malloc((size_t)size);
}

static
void* ecs_os_api_calloc(ecs_size_t size) {
    ecs_os_linc(&ecs_os_api_calloc_count);
    ecs_assert(size > 0, ECS_INVALID_PARAMETER, NULL);
    return calloc(1, (size_t)size);
}

static
void* ecs_os_api_realloc(void *ptr, ecs_size_t size) {
    ecs_assert(size > 0, ECS_INVALID_PARAMETER, NULL);

    if (ptr) {
        ecs_os_linc(&ecs_os_api_realloc_count);
    } else {
        /* If not actually reallocing, treat as malloc */
        ecs_os_linc(&ecs_os_api_malloc_count);
    }
    
    return realloc(ptr, (size_t)size);
}

static
void ecs_os_api_free(void *ptr) {
    if (ptr) {
        ecs_os_linc(&ecs_os_api_free_count);
    }
    free(ptr);
}

static
char* ecs_os_api_strdup(const char *str) {
    if (str) {
        int len = ecs_os_strlen(str);
        char *result = ecs_os_malloc(len + 1);
        ecs_assert(result != NULL, ECS_OUT_OF_MEMORY, NULL);
        ecs_os_strcpy(result, str);
        return result;
    } else {
        return NULL;
    }
}

void ecs_os_strset(char **str, const char *value) {
    char *old = str[0];
    str[0] = ecs_os_strdup(value);
    ecs_os_free(old);
}

/* Replace dots with underscores */
static
char *module_file_base(const char *module, char sep) {
    char *base = ecs_os_strdup(module);
    ecs_size_t i, len = ecs_os_strlen(base);
    for (i = 0; i < len; i ++) {
        if (base[i] == '.') {
            base[i] = sep;
        }
    }

    return base;
}

static
char* ecs_os_api_module_to_dl(const char *module) {
    ecs_strbuf_t lib = ECS_STRBUF_INIT;

    /* Best guess, use module name with underscores + OS library extension */
    char *file_base = module_file_base(module, '_');

#   if defined(ECS_TARGET_LINUX) || defined(ECS_TARGET_FREEBSD)
    ecs_strbuf_appendlit(&lib, "lib");
    ecs_strbuf_appendstr(&lib, file_base);
    ecs_strbuf_appendlit(&lib, ".so");
#   elif defined(ECS_TARGET_DARWIN)
    ecs_strbuf_appendlit(&lib, "lib");
    ecs_strbuf_appendstr(&lib, file_base);
    ecs_strbuf_appendlit(&lib, ".dylib");
#   elif defined(ECS_TARGET_WINDOWS)
    ecs_strbuf_appendstr(&lib, file_base);
    ecs_strbuf_appendlit(&lib, ".dll");
#   endif

    ecs_os_free(file_base);

    return ecs_strbuf_get(&lib);
}

static
char* ecs_os_api_module_to_etc(const char *module) {
    ecs_strbuf_t lib = ECS_STRBUF_INIT;

    /* Best guess, use module name with dashes + /etc */
    char *file_base = module_file_base(module, '-');

    ecs_strbuf_appendstr(&lib, file_base);
    ecs_strbuf_appendlit(&lib, "/etc");

    ecs_os_free(file_base);

    return ecs_strbuf_get(&lib);
}

void ecs_os_set_api_defaults(void)
{
    /* Don't overwrite if already initialized */
    if (ecs_os_api_initialized != 0) {
        return;
    }

    if (ecs_os_api_initializing != 0) {
        return;
    }

    ecs_os_api_initializing = true;
    
    /* Memory management */
    ecs_os_api.malloc_ = ecs_os_api_malloc;
    ecs_os_api.free_ = ecs_os_api_free;
    ecs_os_api.realloc_ = ecs_os_api_realloc;
    ecs_os_api.calloc_ = ecs_os_api_calloc;

    /* Strings */
    ecs_os_api.strdup_ = ecs_os_api_strdup;

    /* Time */
    ecs_os_api.get_time_ = ecs_os_gettime;

    /* Logging */
    ecs_os_api.log_ = flecs_log_msg;

    /* Modules */
    if (!ecs_os_api.module_to_dl_) {
        ecs_os_api.module_to_dl_ = ecs_os_api_module_to_dl;
    }

    if (!ecs_os_api.module_to_etc_) {
        ecs_os_api.module_to_etc_ = ecs_os_api_module_to_etc;
    }

    ecs_os_api.abort_ = abort;

#   ifdef FLECS_OS_API_IMPL
    /* Initialize defaults to OS API IMPL addon, but still allow for overriding
     * by the application */
    ecs_set_os_api_impl();
    ecs_os_api_initialized = false;
#   endif

    ecs_os_api_initializing = false;
}

bool ecs_os_has_heap(void) {
    return 
        (ecs_os_api.malloc_ != NULL) &&
        (ecs_os_api.calloc_ != NULL) &&
        (ecs_os_api.realloc_ != NULL) &&
        (ecs_os_api.free_ != NULL);
}

bool ecs_os_has_threading(void) {
    return
        (ecs_os_api.mutex_new_ != NULL) &&
        (ecs_os_api.mutex_free_ != NULL) &&
        (ecs_os_api.mutex_lock_ != NULL) &&
        (ecs_os_api.mutex_unlock_ != NULL) &&
        (ecs_os_api.cond_new_ != NULL) &&
        (ecs_os_api.cond_free_ != NULL) &&
        (ecs_os_api.cond_wait_ != NULL) &&
        (ecs_os_api.cond_signal_ != NULL) &&
        (ecs_os_api.cond_broadcast_ != NULL) &&
        (ecs_os_api.thread_new_ != NULL) &&
        (ecs_os_api.thread_join_ != NULL) &&
        (ecs_os_api.thread_self_ != NULL);
}

bool ecs_os_has_task_support(void) {
    return
        (ecs_os_api.mutex_new_ != NULL) &&
        (ecs_os_api.mutex_free_ != NULL) &&
        (ecs_os_api.mutex_lock_ != NULL) &&
        (ecs_os_api.mutex_unlock_ != NULL) &&
        (ecs_os_api.cond_new_ != NULL) &&
        (ecs_os_api.cond_free_ != NULL) &&
        (ecs_os_api.cond_wait_ != NULL) &&
        (ecs_os_api.cond_signal_ != NULL) &&
        (ecs_os_api.cond_broadcast_ != NULL) &&
        (ecs_os_api.task_new_ != NULL) &&
        (ecs_os_api.task_join_ != NULL);
}

bool ecs_os_has_time(void) {
    return 
        (ecs_os_api.get_time_ != NULL) &&
        (ecs_os_api.sleep_ != NULL) &&
        (ecs_os_api.now_ != NULL);
}

bool ecs_os_has_logging(void) {
    return (ecs_os_api.log_ != NULL);
}

bool ecs_os_has_dl(void) {
    return 
        (ecs_os_api.dlopen_ != NULL) &&
        (ecs_os_api.dlproc_ != NULL) &&
        (ecs_os_api.dlclose_ != NULL);  
}

bool ecs_os_has_modules(void) {
    return 
        (ecs_os_api.module_to_dl_ != NULL) &&
        (ecs_os_api.module_to_etc_ != NULL);
}

#if defined(ECS_TARGET_WINDOWS)
static char error_str[255];
#endif

const char* ecs_os_strerror(int err) {
#   if defined(ECS_TARGET_WINDOWS)
    strerror_s(error_str, 255, err);
    return error_str;
#   else
    return strerror(err);
#   endif
}

/**
 * @file query.c
 * @brief Cached query implementation.
 * 
 * Cached queries store a list of matched tables. The inputs for a cached query
 * are a filter and an observer. The filter is used to initially populate the
 * cache, and an observer is used to keep the cacne up to date.
 * 
 * Cached queries additionally support features like sorting and grouping. 
 * With sorting, an application can iterate over entities that can be sorted by
 * a component. Grouping allows an application to group matched tables, which is
 * used internally to implement the cascade feature, and can additionally be 
 * used to implement things like world cells.
 */


static
uint64_t flecs_query_get_group_id(
    ecs_query_t *query,
    ecs_table_t *table)
{
    if (query->group_by) {
        return query->group_by(query->filter.world, table, 
            query->group_by_id, query->group_by_ctx);
    } else {
        return 0;
    }
}

static
void flecs_query_compute_group_id(
    ecs_query_t *query,
    ecs_query_table_match_t *match)
{
    ecs_assert(match != NULL, ECS_INTERNAL_ERROR, NULL);

    if (query->group_by) {
        ecs_table_t *table = match->table;
        ecs_assert(table != NULL, ECS_INTERNAL_ERROR, NULL);

        match->group_id = flecs_query_get_group_id(query, table);
    } else {
        match->group_id = 0;
    }
}

static
ecs_query_table_list_t* flecs_query_get_group(
    const ecs_query_t *query,
    uint64_t group_id)
{
    return ecs_map_get_deref(&query->groups, ecs_query_table_list_t, group_id);
}

static
ecs_query_table_list_t* flecs_query_ensure_group(
    ecs_query_t *query,
    uint64_t id)
{
    ecs_query_table_list_t *group = ecs_map_get_deref(&query->groups, 
        ecs_query_table_list_t, id);

    if (!group) {
        group = ecs_map_insert_alloc_t(&query->groups, 
            ecs_query_table_list_t, id);
        ecs_os_zeromem(group);
        if (query->on_group_create) {
            group->info.ctx = query->on_group_create(
                query->filter.world, id, query->group_by_ctx);
        }
    }

    return group;
}

static
void flecs_query_remove_group(
    ecs_query_t *query,
    uint64_t id)
{
    if (query->on_group_delete) {
        ecs_query_table_list_t *group = ecs_map_get_deref(&query->groups, 
            ecs_query_table_list_t, id);
        if (group) {
            query->on_group_delete(query->filter.world, id, 
                group->info.ctx, query->group_by_ctx);
        }
    }

    ecs_map_remove_free(&query->groups, id);
}

static
uint64_t flecs_query_default_group_by(
    ecs_world_t *world, 
    ecs_table_t *table, 
    ecs_id_t id, 
    void *ctx) 
{
    (void)ctx;

    ecs_id_t match;
    if (ecs_search(world, table, ecs_pair(id, EcsWildcard), &match) != -1) {
        return ecs_pair_second(world, match);
    }
    return 0;
}

/* Find the last node of the group after which this group should be inserted */
static
ecs_query_table_match_t* flecs_query_find_group_insertion_node(
    ecs_query_t *query,
    uint64_t group_id)
{
    /* Grouping must be enabled */
    ecs_assert(query->group_by != NULL, ECS_INTERNAL_ERROR, NULL);

    ecs_map_iter_t it = ecs_map_iter(&query->groups);
    ecs_query_table_list_t *list, *closest_list = NULL;
    uint64_t id, closest_id = 0;

    /* Find closest smaller group id */
    while (ecs_map_next(&it)) {
        id = ecs_map_key(&it);
        if (id >= group_id) {
            continue;
        }

        list = ecs_map_ptr(&it);
        if (!list->last) {
            ecs_assert(list->first == NULL, ECS_INTERNAL_ERROR, NULL);
            continue;
        }

        if (!closest_list || ((group_id - id) < (group_id - closest_id))) {
            closest_id = id;
            closest_list = list;
        }
    }

    if (closest_list) {
        return closest_list->last;
    } else {
        return NULL; /* Group should be first in query */
    }
}

/* Initialize group with first node */
static
void flecs_query_create_group(
    ecs_query_t *query,
    ecs_query_table_match_t *match)
{
    uint64_t group_id = match->group_id;

    /* If query has grouping enabled & this is a new/empty group, find
     * the insertion point for the group */
    ecs_query_table_match_t *insert_after = flecs_query_find_group_insertion_node(
        query, group_id);

    if (!insert_after) {
        /* This group should appear first in the query list */
        ecs_query_table_match_t *query_first = query->list.first;
        if (query_first) {
            /* If this is not the first match for the query, insert before it */
            match->next = query_first;
            query_first->prev = match;
            query->list.first = match;
        } else {
            /* If this is the first match of the query, initialize its list */
            ecs_assert(query->list.last == NULL, ECS_INTERNAL_ERROR, NULL);
            query->list.first = match;
            query->list.last = match;
        }
    } else {
        ecs_assert(query->list.first != NULL, ECS_INTERNAL_ERROR, NULL);
        ecs_assert(query->list.last != NULL, ECS_INTERNAL_ERROR, NULL);

        /* This group should appear after another group */
        ecs_query_table_match_t *insert_before = insert_after->next;
        match->prev = insert_after;
        insert_after->next = match;
        match->next = insert_before;
        if (insert_before) {
            insert_before->prev = match;
        } else {
            ecs_assert(query->list.last == insert_after, 
                ECS_INTERNAL_ERROR, NULL);
                
            /* This group should appear last in the query list */
            query->list.last = match;
        }
    }
}

/* Find the list the node should be part of */
static
ecs_query_table_list_t* flecs_query_get_node_list(
    ecs_query_t *query,
    ecs_query_table_match_t *match)
{
    if (query->group_by) {
        return flecs_query_get_group(query, match->group_id);
    } else {
        return &query->list;
    }
}

/* Find or create the list the node should be part of */
static
ecs_query_table_list_t* flecs_query_ensure_node_list(
    ecs_query_t *query,
    ecs_query_table_match_t *match)
{
    if (query->group_by) {
        return flecs_query_ensure_group(query, match->group_id);
    } else {
        return &query->list;
    }
}

/* Remove node from list */
static
void flecs_query_remove_table_node(
    ecs_query_t *query,
    ecs_query_table_match_t *match)
{
    ecs_query_table_match_t *prev = match->prev;
    ecs_query_table_match_t *next = match->next;

    ecs_assert(prev != match, ECS_INTERNAL_ERROR, NULL);
    ecs_assert(next != match, ECS_INTERNAL_ERROR, NULL);
    ecs_assert(!prev || prev != next, ECS_INTERNAL_ERROR, NULL);

    ecs_query_table_list_t *list = flecs_query_get_node_list(query, match);

    if (!list || !list->first) {
        /* If list contains no matches, the match must be empty */
        ecs_assert(!list || list->last == NULL, ECS_INTERNAL_ERROR, NULL);
        ecs_assert(prev == NULL, ECS_INTERNAL_ERROR, NULL);
        ecs_assert(next == NULL, ECS_INTERNAL_ERROR, NULL);
        return;
    }

    ecs_assert(prev != NULL || query->list.first == match, 
        ECS_INTERNAL_ERROR, NULL);
    ecs_assert(next != NULL || query->list.last == match, 
        ECS_INTERNAL_ERROR, NULL);

    if (prev) {
        prev->next = next;
    }
    if (next) {
        next->prev = prev;
    }

    ecs_assert(list->info.table_count > 0, ECS_INTERNAL_ERROR, NULL);
    list->info.table_count --;

    if (query->group_by) {
        uint64_t group_id = match->group_id;

        /* Make sure query.list is updated if this is the first or last group */
        if (query->list.first == match) {
            ecs_assert(prev == NULL, ECS_INTERNAL_ERROR, NULL);
            query->list.first = next;
            prev = next;
        }
        if (query->list.last == match) {
            ecs_assert(next == NULL, ECS_INTERNAL_ERROR, NULL);
            query->list.last = prev;
            next = prev;
        }

        ecs_assert(query->list.info.table_count > 0, ECS_INTERNAL_ERROR, NULL);
        query->list.info.table_count --;
        list->info.match_count ++;

        /* Make sure group list only contains nodes that belong to the group */
        if (prev && prev->group_id != group_id) {
            /* The previous node belonged to another group */
            prev = next;
        }
        if (next && next->group_id != group_id) {
            /* The next node belonged to another group */
            next = prev;
        }

        /* Do check again, in case both prev & next belonged to another group */
        if ((!prev && !next) || (prev && prev->group_id != group_id)) {
            /* There are no more matches left in this group */
            flecs_query_remove_group(query, group_id);
            list = NULL;
        }
    }

    if (list) {
        if (list->first == match) {
            list->first = next;
        }
        if (list->last == match) {
            list->last = prev;
        }
    }

    match->prev = NULL;
    match->next = NULL;

    query->match_count ++;
}

/* Add node to list */
static
void flecs_query_insert_table_node(
    ecs_query_t *query,
    ecs_query_table_match_t *match)
{
    /* Node should not be part of an existing list */
    ecs_assert(match->prev == NULL && match->next == NULL, 
        ECS_INTERNAL_ERROR, NULL);

    /* If this is the first match, activate system */
    if (!query->list.first && query->filter.entity) {
        ecs_remove_id(query->filter.world, query->filter.entity, EcsEmpty);
    }

    flecs_query_compute_group_id(query, match);

    ecs_query_table_list_t *list = flecs_query_ensure_node_list(query, match);
    if (list->last) {
        ecs_assert(query->list.first != NULL, ECS_INTERNAL_ERROR, NULL);
        ecs_assert(query->list.last != NULL, ECS_INTERNAL_ERROR, NULL);
        ecs_assert(list->first != NULL, ECS_INTERNAL_ERROR, NULL);

        ecs_query_table_match_t *last = list->last;
        ecs_query_table_match_t *last_next = last->next;

        match->prev = last;
        match->next = last_next;
        last->next = match;

        if (last_next) {
            last_next->prev = match;
        }

        list->last = match;

        if (query->group_by) {
            /* Make sure to update query list if this is the last group */
            if (query->list.last == last) {
                query->list.last = match;
            }
        }
    } else {
        ecs_assert(list->first == NULL, ECS_INTERNAL_ERROR, NULL);

        list->first = match;
        list->last = match;

        if (query->group_by) {
            /* Initialize group with its first node */
            flecs_query_create_group(query, match);
        }
    }

    if (query->group_by) {
        list->info.table_count ++;
        list->info.match_count ++;
    }

    query->list.info.table_count ++;
    query->match_count ++;

    ecs_assert(match->prev != match, ECS_INTERNAL_ERROR, NULL);
    ecs_assert(match->next != match, ECS_INTERNAL_ERROR, NULL);

    ecs_assert(list->first != NULL, ECS_INTERNAL_ERROR, NULL);
    ecs_assert(list->last != NULL, ECS_INTERNAL_ERROR, NULL);
    ecs_assert(list->last == match, ECS_INTERNAL_ERROR, NULL);
    ecs_assert(query->list.first != NULL, ECS_INTERNAL_ERROR, NULL);
    ecs_assert(query->list.last != NULL, ECS_INTERNAL_ERROR, NULL);
    ecs_assert(query->list.first->prev == NULL, ECS_INTERNAL_ERROR, NULL);
    ecs_assert(query->list.last->next == NULL, ECS_INTERNAL_ERROR, NULL);
}

static
ecs_query_table_match_t* flecs_query_cache_add(
    ecs_world_t *world,
    ecs_query_table_t *elem)
{
    ecs_query_table_match_t *result = 
        flecs_bcalloc(&world->allocators.query_table_match);

    if (!elem->first) {
        elem->first = result;
        elem->last = result;
    } else {
        ecs_assert(elem->last != NULL, ECS_INTERNAL_ERROR, NULL);
        elem->last->next_match = result;
        elem->last = result;
    }

    return result;
}

typedef struct {
    ecs_table_t *table;
    int32_t *dirty_state;
    int32_t column;
} table_dirty_state_t;

static
void flecs_query_get_dirty_state(
    ecs_query_t *query,
    ecs_query_table_match_t *match,
    int32_t term,
    table_dirty_state_t *out)
{
    ecs_world_t *world = query->filter.world;
    ecs_entity_t subject = match->sources[term];
    ecs_table_t *table;
    int32_t column = -1;

    if (!subject) {
        table = match->table;
        column = match->storage_columns[term];
    } else {
        table = ecs_get_table(world, subject);
        int32_t ref_index = -match->columns[term] - 1;
        ecs_ref_t *ref = ecs_vec_get_t(&match->refs, ecs_ref_t, ref_index);
        if (ref->id != 0) {
            ecs_ref_update(world, ref);
            column = ref->tr->column;
            column = ecs_table_type_to_storage_index(table, column);
        }
    }

    out->table = table;
    out->column = column;
    out->dirty_state = flecs_table_get_dirty_state(world, table);
}

/* Get match monitor. Monitors are used to keep track of whether components 
 * matched by the query in a table have changed. */
static
bool flecs_query_get_match_monitor(
    ecs_query_t *query,
    ecs_query_table_match_t *match)
{
    if (match->monitor) {
        return false;
    }

    int32_t *monitor = flecs_balloc(&query->allocators.monitors);
    monitor[0] = 0;

    /* Mark terms that don't need to be monitored. This saves time when reading
     * and/or updating the monitor. */
    const ecs_filter_t *f = &query->filter;
    int32_t i, t = -1, term_count = f->term_count;
    table_dirty_state_t cur_dirty_state;

    for (i = 0; i < term_count; i ++) {
        if (t == f->terms[i].field_index) {
            if (monitor[t + 1] != -1) {
                continue;
            }
        }

        t = f->terms[i].field_index;
        monitor[t + 1] = -1;

        if (f->terms[i].inout != EcsIn && 
            f->terms[i].inout != EcsInOut &&
            f->terms[i].inout != EcsInOutDefault) {
            continue; /* If term isn't read, don't monitor */
        }

        /* If term is not matched on this, don't track */
        if (!ecs_term_match_this(&f->terms[i])) {
            continue;
        }

        int32_t column = match->columns[t];
        if (column == 0) {
            continue; /* Don't track terms that aren't matched */
        }

        flecs_query_get_dirty_state(query, match, t, &cur_dirty_state);
        if (cur_dirty_state.column == -1) {
            continue; /* Don't track terms that aren't stored */
        }

        monitor[t + 1] = 0;
    }


    /* If matched table needs entity filter, make sure to test fields that could
     * be matched by flattened parents. */
    ecs_entity_filter_t *ef = match->entity_filter;
    if (ef && ef->flat_tree_column != -1) {
        int32_t *fields = ecs_vec_first(&ef->ft_terms);
        int32_t field_count = ecs_vec_count(&ef->ft_terms);
        for (i = 0; i < field_count; i ++) {
            monitor[fields[i] + 1] = 0;
        }
    }

    match->monitor = monitor;

    query->flags |= EcsQueryHasMonitor;

    return true;
}

/* Synchronize match monitor with table dirty state */
static
void flecs_query_sync_match_monitor(
    ecs_query_t *query,
    ecs_query_table_match_t *match)
{
    ecs_assert(match != NULL, ECS_INTERNAL_ERROR, NULL);
    if (!match->monitor) {
        if (query->flags & EcsQueryHasMonitor) {
            flecs_query_get_match_monitor(query, match);
        } else {
            return;
        }
    }

    int32_t *monitor = match->monitor;
    ecs_table_t *table = match->table;
    if (table) {
        int32_t *dirty_state = flecs_table_get_dirty_state(
            query->filter.world, table);
        ecs_assert(dirty_state != NULL, ECS_INTERNAL_ERROR, NULL);
        monitor[0] = dirty_state[0]; /* Did table gain/lose entities */
    }

    table_dirty_state_t cur;
    int32_t i, term_count = query->filter.term_count;
    for (i = 0; i < term_count; i ++) {
        int32_t t = query->filter.terms[i].field_index;
        if (monitor[t + 1] == -1) {
            continue;
        }
                
        flecs_query_get_dirty_state(query, match, t, &cur);
        if (cur.column < 0) {
            continue;
        }

        monitor[t + 1] = cur.dirty_state[cur.column + 1];
    }

    ecs_entity_filter_t *ef = match->entity_filter;
    if (ef && ef->flat_tree_column != -1) {
        flecs_flat_table_term_t *fields = ecs_vec_first(&ef->ft_terms);
        int32_t field_count = ecs_vec_count(&ef->ft_terms);
        for (i = 0; i < field_count; i ++) {
            flecs_flat_table_term_t *field = &fields[i];
            flecs_flat_monitor_t *tgt_mon = ecs_vec_first(&field->monitor);
            int32_t t, tgt_count = ecs_vec_count(&field->monitor);
            for (t = 0; t < tgt_count; t ++) {
                tgt_mon[t].monitor = tgt_mon[t].table_state;
            }
        }
    }

    query->prev_match_count = query->match_count;
}

/* Check if single match term has changed */
static
bool flecs_query_check_match_monitor_term(
    ecs_query_t *query,
    ecs_query_table_match_t *match,
    int32_t term)
{
    ecs_assert(match != NULL, ECS_INTERNAL_ERROR, NULL);

    if (flecs_query_get_match_monitor(query, match)) {
        return true;
    }
    
    int32_t *monitor = match->monitor;
    int32_t state = monitor[term];
    if (state == -1) {
        return false;
    }

    ecs_table_t *table = match->table;
    if (table) {
        int32_t *dirty_state = flecs_table_get_dirty_state(
            query->filter.world, table);
        ecs_assert(dirty_state != NULL, ECS_INTERNAL_ERROR, NULL);
        if (!term) {
            return monitor[0] != dirty_state[0];
        }
    } else if (!term) {
        return false;
    }

    table_dirty_state_t cur;
    flecs_query_get_dirty_state(query, match, term - 1, &cur);
    ecs_assert(cur.column != -1, ECS_INTERNAL_ERROR, NULL);

    return monitor[term] != cur.dirty_state[cur.column + 1];
}

/* Check if any term for match has changed */
static
bool flecs_query_check_match_monitor(
    ecs_query_t *query,
    ecs_query_table_match_t *match,
    const ecs_iter_t *it)
{
    ecs_assert(match != NULL, ECS_INTERNAL_ERROR, NULL);

    if (flecs_query_get_match_monitor(query, match)) {
        return true;
    }

    int32_t *monitor = match->monitor;
    ecs_table_t *table = match->table;
    int32_t *dirty_state = NULL;
    if (table) {
        dirty_state = flecs_table_get_dirty_state(
            query->filter.world, table);
        ecs_assert(dirty_state != NULL, ECS_INTERNAL_ERROR, NULL);
        if (monitor[0] != dirty_state[0]) {
            return true;
        }
    }

    bool has_flat = false;
    ecs_world_t *world = query->filter.world;
    int32_t i, field_count = query->filter.field_count;
    int32_t *storage_columns = match->storage_columns;
    int32_t *columns = it ? it->columns : NULL;
    if (!columns) {
        columns = match->columns;
    }
    ecs_vec_t *refs = &match->refs;
    for (i = 0; i < field_count; i ++) {
        int32_t mon = monitor[i + 1];
        if (mon == -1) {
            continue;
        }

        int32_t column = storage_columns[i];
        if (column >= 0) {
            /* owned component */
            ecs_assert(dirty_state != NULL, ECS_INTERNAL_ERROR, NULL);
            if (mon != dirty_state[column + 1]) {
                return true;
            }
            continue;
        } else if (column == -1) {
            continue; /* owned but not a component */
        }

        column = columns[i];
        if (!column) {
            /* Not matched */
            continue;
        }

        ecs_assert(column < 0, ECS_INTERNAL_ERROR, NULL);
        column = -column;

        /* Flattened fields are encoded by adding field_count to the column
         * index of the parent component. */
        if (it && (column > field_count)) {
            has_flat = true;
        } else {
            int32_t ref_index = column - 1;
            ecs_ref_t *ref = ecs_vec_get_t(refs, ecs_ref_t, ref_index);
            if (ref->id != 0) {
                ecs_ref_update(world, ref);
                ecs_table_record_t *tr = ref->tr;
                ecs_table_t *src_table = tr->hdr.table;
                column = tr->column;
                column = ecs_table_type_to_storage_index(src_table, column);
                int32_t *src_dirty_state = flecs_table_get_dirty_state(
                    world, src_table);
                if (mon != src_dirty_state[column + 1]) {
                    return true;
                }
            }
        }
    }

    if (has_flat) {
        ecs_entity_filter_t *ef = match->entity_filter;
        flecs_flat_table_term_t *fields = ecs_vec_first(&ef->ft_terms);
        ecs_entity_filter_iter_t *ent_it = it->priv.entity_iter;
        int32_t cur_tgt = ent_it->target_count - 1;
        field_count = ecs_vec_count(&ef->ft_terms);

        for (i = 0; i < field_count; i ++) {
            flecs_flat_table_term_t *field = &fields[i];
            flecs_flat_monitor_t *fmon = ecs_vec_get_t(&field->monitor, 
                flecs_flat_monitor_t, cur_tgt);
            if (fmon->monitor != fmon->table_state) {
                return true;
            }
        }
    }

    return false;
}

/* Check if any term for matched table has changed */
static
bool flecs_query_check_table_monitor(
    ecs_query_t *query,
    ecs_query_table_t *table,
    int32_t term)
{
    ecs_query_table_match_t *cur, *end = table->last->next;

    for (cur = table->first; cur != end; cur = cur->next) {
        ecs_query_table_match_t *match = (ecs_query_table_match_t*)cur;
        if (term == -1) {
            if (flecs_query_check_match_monitor(query, match, NULL)) {
                return true;
            }
        } else {
            if (flecs_query_check_match_monitor_term(query, match, term)) {
                return true;
            } 
        }
    }

    return false;
}

static
bool flecs_query_check_query_monitor(
    ecs_query_t *query)
{
    ecs_table_cache_iter_t it;
    if (flecs_table_cache_iter(&query->cache, &it)) {
        ecs_query_table_t *qt;
        while ((qt = flecs_table_cache_next(&it, ecs_query_table_t))) {
            if (flecs_query_check_table_monitor(query, qt, -1)) {
                return true;
            }
        }
    }

    return false;
}

static
void flecs_query_init_query_monitors(
    ecs_query_t *query)
{
    ecs_query_table_match_t *cur = query->list.first;

    /* Ensure each match has a monitor */
    for (; cur != NULL; cur = cur->next) {
        ecs_query_table_match_t *match = (ecs_query_table_match_t*)cur;
        flecs_query_get_match_monitor(query, match);
    }
}

/* The group by function for cascade computes the tree depth for the table type.
 * This causes tables in the query cache to be ordered by depth, which ensures
 * breadth-first iteration order. */
static
uint64_t flecs_query_group_by_cascade(
    ecs_world_t *world,
    ecs_table_t *table,
    ecs_id_t id,
    void *ctx)
{
    (void)id;
    ecs_term_t *term = ctx;
    ecs_entity_t rel = term->src.trav;
    int32_t depth = flecs_relation_depth(world, rel, table);
    return flecs_ito(uint64_t, depth);
}

static
void flecs_query_add_ref(
    ecs_world_t *world,
    ecs_query_t *query,
    ecs_query_table_match_t *qm,
    ecs_entity_t component,
    ecs_entity_t entity,
    ecs_size_t size)
{
    ecs_assert(entity != 0, ECS_INTERNAL_ERROR, NULL);
    ecs_ref_t *ref = ecs_vec_append_t(&world->allocator, &qm->refs, ecs_ref_t);
    ecs_assert(entity != 0, ECS_INTERNAL_ERROR, NULL);
    
    if (size) {
        *ref = ecs_ref_init_id(world, entity, component);
    } else {
        *ref = (ecs_ref_t){
            .entity = entity,
            .id = 0
        };
    }

    query->flags |= EcsQueryHasRefs;
}

static
ecs_query_table_match_t* flecs_query_add_table_match(
    ecs_query_t *query,
    ecs_query_table_t *qt,
    ecs_table_t *table)
{
    /* Add match for table. One table can have more than one match, if
     * the query contains wildcards. */
    ecs_query_table_match_t *qm = flecs_query_cache_add(query->filter.world, qt);
    qm->table = table;

    qm->columns = flecs_balloc(&query->allocators.columns);
    qm->storage_columns = flecs_balloc(&query->allocators.columns);
    qm->ids = flecs_balloc(&query->allocators.ids);
    qm->sources = flecs_balloc(&query->allocators.sources);

    /* Insert match to iteration list if table is not empty */
    if (!table || ecs_table_count(table) != 0) {
        flecs_query_insert_table_node(query, qm);
    }

    return qm;
}

static
void flecs_query_set_table_match(
    ecs_world_t *world,
    ecs_query_t *query,
    ecs_query_table_match_t *qm,
    ecs_table_t *table,
    ecs_iter_t *it)
{
    ecs_allocator_t *a = &world->allocator;
    ecs_filter_t *filter = &query->filter;
    int32_t i, term_count = filter->term_count;
    int32_t field_count = filter->field_count;
    ecs_term_t *terms = filter->terms;

    /* Reset resources in case this is an existing record */
    ecs_vec_reset_t(a, &qm->refs, ecs_ref_t);
    ecs_os_memcpy_n(qm->columns, it->columns, int32_t, field_count);
    ecs_os_memcpy_n(qm->ids, it->ids, ecs_id_t, field_count);
    ecs_os_memcpy_n(qm->sources, it->sources, ecs_entity_t, field_count);

    if (table) {
        /* Initialize storage columns for faster access to component storage */
        for (i = 0; i < field_count; i ++) {
            if (terms[i].inout == EcsInOutNone) {
                qm->storage_columns[i] = -1;
                continue;
            }

            int32_t column = qm->columns[i];
            if (column > 0) {
                qm->storage_columns[i] = ecs_table_type_to_storage_index(table,
                    qm->columns[i] - 1);
            } else {
                /* Shared field (not from table) */
                qm->storage_columns[i] = -2;
            }
        }

        flecs_entity_filter_init(world, &qm->entity_filter, filter, 
            table, qm->ids, qm->columns);

        if (qm->entity_filter) {
            query->flags &= ~EcsQueryTrivialIter;
        }
        if (table->flags & EcsTableHasUnion) {
            query->flags &= ~EcsQueryTrivialIter;
        }
    }

    /* Add references for substituted terms */
    for (i = 0; i < term_count; i ++) {
        ecs_term_t *term = &terms[i];
        if (!ecs_term_match_this(term)) {
            /* non-This terms are set during iteration */
            continue;
        }

        int32_t field = terms[i].field_index;
        ecs_entity_t src = it->sources[field];
        ecs_size_t size = 0;
        if (it->sizes) {
            size = it->sizes[field];
        }
        if (src) {
            ecs_id_t id = it->ids[field];
            ecs_assert(ecs_is_valid(world, src), ECS_INTERNAL_ERROR, NULL);

            if (id) {
                flecs_query_add_ref(world, query, qm, id, src, size);

                /* Use column index to bind term and ref */
                if (qm->columns[field] != 0) {
                    qm->columns[field] = -ecs_vec_count(&qm->refs);
                }
            }
        }
    }
}

static
ecs_query_table_t* flecs_query_table_insert(
    ecs_world_t *world,
    ecs_query_t *query,
    ecs_table_t *table)
{
    ecs_query_table_t *qt = flecs_bcalloc(&world->allocators.query_table);
    if (table) {
        qt->table_id = table->id;
    } else {
        qt->table_id = 0;
    }
    ecs_table_cache_insert(&query->cache, table, &qt->hdr);
    return qt;
}

/** Populate query cache with tables */
static
void flecs_query_match_tables(
    ecs_world_t *world,
    ecs_query_t *query)
{
    ecs_table_t *table = NULL;
    ecs_query_table_t *qt = NULL;

    ecs_iter_t it = ecs_filter_iter(world, &query->filter);
    ECS_BIT_SET(it.flags, EcsIterIsInstanced);
    ECS_BIT_SET(it.flags, EcsIterNoData);
    ECS_BIT_SET(it.flags, EcsIterTableOnly);
    ECS_BIT_SET(it.flags, EcsIterEntityOptional);

    while (ecs_filter_next(&it)) {
        if ((table != it.table) || (!it.table && !qt)) {
            /* New table matched, add record to cache */
            table = it.table;
            qt = flecs_query_table_insert(world, query, table);
        }

        ecs_query_table_match_t *qm = flecs_query_add_table_match(query, qt, table);
        flecs_query_set_table_match(world, query, qm, table, &it);
    }
}

static
bool flecs_query_match_table(
    ecs_world_t *world,
    ecs_query_t *query,
    ecs_table_t *table)
{
    if (!ecs_map_is_init(&query->cache.index)) {
        return false;
    }

    ecs_query_table_t *qt = NULL;
    ecs_filter_t *filter = &query->filter;
    int var_id = ecs_filter_find_this_var(filter);
    if (var_id == -1) {
        /* If query doesn't match with This term, it can't match with tables */
        return false;
    }

    ecs_iter_t it = flecs_filter_iter_w_flags(world, filter, EcsIterMatchVar|
        EcsIterIsInstanced|EcsIterNoData|EcsIterEntityOptional);
    ecs_iter_set_var_as_table(&it, var_id, table);

    while (ecs_filter_next(&it)) {
        ecs_assert(it.table == table, ECS_INTERNAL_ERROR, NULL);
        if (qt == NULL) {
            table = it.table;
            qt = flecs_query_table_insert(world, query, table);
        }

        ecs_query_table_match_t *qm = flecs_query_add_table_match(query, qt, table);
        flecs_query_set_table_match(world, query, qm, table, &it);
    }

    return qt != NULL;
}

ECS_SORT_TABLE_WITH_COMPARE(_, flecs_query_sort_table_generic, order_by, static)

static
void flecs_query_sort_table(
    ecs_world_t *world,
    ecs_table_t *table,
    int32_t column_index,
    ecs_order_by_action_t compare,
    ecs_sort_table_action_t sort)
{
    ecs_data_t *data = &table->data;
    if (!ecs_vec_count(&data->entities)) {
        /* Nothing to sort */
        return;
    }

    int32_t count = flecs_table_data_count(data);
    if (count < 2) {
        return;
    }

    ecs_entity_t *entities = ecs_vec_first(&data->entities);

    void *ptr = NULL;
    int32_t size = 0;
    if (column_index != -1) {
        ecs_type_info_t *ti = table->type_info[column_index];
        ecs_vec_t *column = &data->columns[column_index];
        size = ti->size;
        ptr = ecs_vec_first(column);
    }

    if (sort) {
        sort(world, table, entities, ptr, size, 0, count - 1, compare);
    } else {
        flecs_query_sort_table_generic(world, table, entities, ptr, size, 0, count - 1, compare);
    }
}

/* Helper struct for building sorted table ranges */
typedef struct sort_helper_t {
    ecs_query_table_match_t *match;
    ecs_entity_t *entities;
    const void *ptr;
    int32_t row;
    int32_t elem_size;
    int32_t count;
    bool shared;
} sort_helper_t;

static
const void* ptr_from_helper(
    sort_helper_t *helper)
{
    ecs_assert(helper->row < helper->count, ECS_INTERNAL_ERROR, NULL);
    ecs_assert(helper->elem_size >= 0, ECS_INTERNAL_ERROR, NULL);
    ecs_assert(helper->row >= 0, ECS_INTERNAL_ERROR, NULL);
    if (helper->shared) {
        return helper->ptr;
    } else {
        return ECS_ELEM(helper->ptr, helper->elem_size, helper->row);
    }
}

static
ecs_entity_t e_from_helper(
    sort_helper_t *helper)
{
    if (helper->row < helper->count) {
        return helper->entities[helper->row];
    } else {
        return 0;
    }
}

static
void flecs_query_build_sorted_table_range(
    ecs_query_t *query,
    ecs_query_table_list_t *list)
{
    ecs_world_t *world = query->filter.world;
    ecs_assert(!(world->flags & EcsWorldMultiThreaded), ECS_UNSUPPORTED,
        "cannot sort query in multithreaded mode");

    ecs_entity_t id = query->order_by_component;
    ecs_order_by_action_t compare = query->order_by;
    int32_t table_count = list->info.table_count;
    if (!table_count) {
        return;
    }

    ecs_vec_init_if_t(&query->table_slices, ecs_query_table_match_t);
    int32_t to_sort = 0;
    int32_t order_by_term = query->order_by_term;

    sort_helper_t *helper = flecs_alloc_n(
        &world->allocator, sort_helper_t, table_count);
    ecs_query_table_match_t *cur, *end = list->last->next;
    for (cur = list->first; cur != end; cur = cur->next) {
        ecs_table_t *table = cur->table;
        ecs_data_t *data = &table->data;

        ecs_assert(ecs_table_count(table) != 0, ECS_INTERNAL_ERROR, NULL);

        if (id) {
            const ecs_term_t *term = &query->filter.terms[order_by_term];
            int32_t field = term->field_index;
            int32_t column = cur->columns[field];
            ecs_size_t size = query->filter.sizes[field];
            ecs_assert(column != 0, ECS_INTERNAL_ERROR, NULL);
            if (column >= 0) {
                column = table->storage_map[column - 1];
                ecs_vec_t *vec = &data->columns[column];
                helper[to_sort].ptr = ecs_vec_first(vec);
                helper[to_sort].elem_size = size;
                helper[to_sort].shared = false;
            } else {
                ecs_entity_t src = cur->sources[field];
                ecs_assert(src != 0, ECS_INTERNAL_ERROR, NULL);
                ecs_record_t *r = flecs_entities_get(world, src);
                ecs_assert(r != NULL, ECS_INTERNAL_ERROR, NULL);
                ecs_assert(r->table != NULL, ECS_INTERNAL_ERROR, NULL);

                if (term->src.flags & EcsUp) {
                    ecs_entity_t base = 0;
                    ecs_search_relation(world, r->table, 0, id, 
                        EcsIsA, term->src.flags & EcsTraverseFlags, &base, 0, 0);
                    if (base && base != src) { /* Component could be inherited */
                        r = flecs_entities_get(world, base);
                    }
                }

                helper[to_sort].ptr = ecs_table_get_id(
                    world, r->table, id, ECS_RECORD_TO_ROW(r->row));
                helper[to_sort].elem_size = size;
                helper[to_sort].shared = true;
            }
            ecs_assert(helper[to_sort].ptr != NULL, ECS_INTERNAL_ERROR, NULL);
            ecs_assert(helper[to_sort].elem_size != 0, ECS_INTERNAL_ERROR, NULL);
        } else {
            helper[to_sort].ptr = NULL;
            helper[to_sort].elem_size = 0;
            helper[to_sort].shared = false;
        }

        helper[to_sort].match = cur;
        helper[to_sort].entities = ecs_vec_first(&data->entities);
        helper[to_sort].row = 0;
        helper[to_sort].count = ecs_table_count(table);
        to_sort ++;      
    }

    ecs_assert(to_sort != 0, ECS_INTERNAL_ERROR, NULL);

    bool proceed;
    do {
        int32_t j, min = 0;
        proceed = true;

        ecs_entity_t e1;
        while (!(e1 = e_from_helper(&helper[min]))) {
            min ++;
            if (min == to_sort) {
                proceed = false;
                break;
            }
        }

        if (!proceed) {
            break;
        }

        for (j = min + 1; j < to_sort; j++) {
            ecs_entity_t e2 = e_from_helper(&helper[j]);
            if (!e2) {
                continue;
            }

            const void *ptr1 = ptr_from_helper(&helper[min]);
            const void *ptr2 = ptr_from_helper(&helper[j]);

            if (compare(e1, ptr1, e2, ptr2) > 0) {
                min = j;
                e1 = e_from_helper(&helper[min]);
            }
        }

        sort_helper_t *cur_helper = &helper[min];
        if (!cur || cur->columns != cur_helper->match->columns) {
            cur = ecs_vec_append_t(NULL, &query->table_slices, 
                ecs_query_table_match_t);
            *cur = *(cur_helper->match);
            cur->offset = cur_helper->row;
            cur->count = 1;
        } else {
            cur->count ++;
        }

        cur_helper->row ++;
    } while (proceed);

    /* Iterate through the vector of slices to set the prev/next ptrs. This
     * can't be done while building the vector, as reallocs may occur */
    int32_t i, count = ecs_vec_count(&query->table_slices);    
    ecs_query_table_match_t *nodes = ecs_vec_first(&query->table_slices);
    for (i = 0; i < count; i ++) {
        nodes[i].prev = &nodes[i - 1];
        nodes[i].next = &nodes[i + 1];
    }

    nodes[0].prev = NULL;
    nodes[i - 1].next = NULL;

    flecs_free_n(&world->allocator, sort_helper_t, table_count, helper);
}

static
void flecs_query_build_sorted_tables(
    ecs_query_t *query)
{
    ecs_vec_clear(&query->table_slices);

    if (query->group_by) {
        /* Populate sorted node list in grouping order */
        ecs_query_table_match_t *cur = query->list.first;
        if (cur) {
            do {
                /* Find list for current group */
                uint64_t group_id = cur->group_id;
                ecs_query_table_list_t *list = ecs_map_get_deref(
                    &query->groups, ecs_query_table_list_t, group_id);
                ecs_assert(list != NULL, ECS_INTERNAL_ERROR, NULL);

                /* Sort tables in current group */
                flecs_query_build_sorted_table_range(query, list);

                /* Find next group to sort */
                cur = list->last->next;
            } while (cur);
        }
    } else {
        flecs_query_build_sorted_table_range(query, &query->list);
    }
}

static
void flecs_query_sort_tables(
    ecs_world_t *world,
    ecs_query_t *query)
{
    ecs_order_by_action_t compare = query->order_by;
    if (!compare) {
        return;
    }

    ecs_sort_table_action_t sort = query->sort_table;
    
    ecs_entity_t order_by_component = query->order_by_component;
    int32_t order_by_term = query->order_by_term;

    /* Iterate over non-empty tables. Don't bother with empty tables as they
     * have nothing to sort */

    bool tables_sorted = false;

    ecs_table_cache_iter_t it;
    ecs_query_table_t *qt;
    flecs_table_cache_iter(&query->cache, &it);

    while ((qt = flecs_table_cache_next(&it, ecs_query_table_t))) {
        ecs_table_t *table = qt->hdr.table;
        bool dirty = false;

        if (flecs_query_check_table_monitor(query, qt, 0)) {
            dirty = true;
        }

        int32_t column = -1;
        if (order_by_component) {
            if (flecs_query_check_table_monitor(query, qt, order_by_term + 1)) {
                dirty = true;
            }

            if (dirty) {
                column = -1;

                ecs_table_t *storage_table = table->storage_table;
                if (storage_table) {
                    column = ecs_search(world, storage_table, 
                        order_by_component, 0);
                }

                if (column == -1) {
                    /* Component is shared, no sorting is needed */
                    dirty = false;
                }
            }
        }

        if (!dirty) {
            continue;
        }

        /* Something has changed, sort the table. Prefers using 
         * flecs_query_sort_table when available */
        flecs_query_sort_table(world, table, column, compare, sort);
        tables_sorted = true;
    }

    if (tables_sorted || query->match_count != query->prev_match_count) {
        flecs_query_build_sorted_tables(query);
        query->match_count ++; /* Increase version if tables changed */
    }
}

static
bool flecs_query_has_refs(
    ecs_query_t *query)
{
    ecs_term_t *terms = query->filter.terms;
    int32_t i, count = query->filter.term_count;
    for (i = 0; i < count; i ++) {
        if (terms[i].src.flags & (EcsUp | EcsIsEntity)) {
            return true;
        }
    }

    return false;
}

static
void flecs_query_for_each_component_monitor(
    ecs_world_t *world,
    ecs_query_t *query,
    void(*callback)(
        ecs_world_t* world,
        ecs_id_t id,
        ecs_query_t *query))
{
    ecs_term_t *terms = query->filter.terms;
    int32_t i, count = query->filter.term_count;

    for (i = 0; i < count; i++) {
        ecs_term_t *term = &terms[i];
        ecs_term_id_t *src = &term->src;

        if (src->flags & EcsUp) {
            callback(world, ecs_pair(src->trav, EcsWildcard), query);
            if (src->trav != EcsIsA) {
                callback(world, ecs_pair(EcsIsA, EcsWildcard), query);
            }
            callback(world, term->id, query);

        } else if (src->flags & EcsSelf && !ecs_term_match_this(term)) {
            callback(world, term->id, query);
        }
    }
}

static
bool flecs_query_is_term_id_supported(
    ecs_term_id_t *term_id)
{
    if (!(term_id->flags & EcsIsVariable)) {
        return true;
    }
    if (ecs_id_is_wildcard(term_id->id)) {
        return true;
    }
    return false;
}

static
int flecs_query_process_signature(
    ecs_world_t *world,
    ecs_query_t *query)
{
    ecs_term_t *terms = query->filter.terms;
    int32_t i, count = query->filter.term_count;

    for (i = 0; i < count; i ++) {
        ecs_term_t *term = &terms[i];
        ecs_term_id_t *first = &term->first;
        ecs_term_id_t *src = &term->src;
        ecs_term_id_t *second = &term->second;
        ecs_inout_kind_t inout = term->inout;

        bool is_src_ok = flecs_query_is_term_id_supported(src);
        bool is_first_ok = flecs_query_is_term_id_supported(first);
        bool is_second_ok = flecs_query_is_term_id_supported(second);

        (void)first;
        (void)second;
        (void)is_src_ok;
        (void)is_first_ok;
        (void)is_second_ok;

        /* Queries do not support named variables */
        ecs_check(is_src_ok || ecs_term_match_this(term),
            ECS_UNSUPPORTED, NULL);
        ecs_check(is_first_ok, ECS_UNSUPPORTED, NULL);
        ecs_check(is_second_ok,  ECS_UNSUPPORTED, NULL);
        ecs_check(!(src->flags & EcsFilter), ECS_INVALID_PARAMETER,
            "invalid usage of Filter for query");

        if (inout != EcsIn && inout != EcsInOutNone) {
            query->flags |= EcsQueryHasOutColumns;
        }

        if (src->flags & EcsCascade) {
            /* Query can only have one cascade column */
            ecs_assert(query->cascade_by == 0, ECS_INVALID_PARAMETER, NULL);
            query->cascade_by = i + 1;
        }
    }

    query->flags |= (ecs_flags32_t)(flecs_query_has_refs(query) * EcsQueryHasRefs);

    if (!(query->flags & EcsQueryIsSubquery)) {
        flecs_query_for_each_component_monitor(world, query, flecs_monitor_register);
    }

    return 0;
error:
    return -1;
}

/** When a table becomes empty remove it from the query list, or vice versa. */
static
void flecs_query_update_table(
    ecs_query_t *query,
    ecs_table_t *table,
    bool empty)
{
    int32_t prev_count = ecs_query_table_count(query);
    ecs_table_cache_set_empty(&query->cache, table, empty);
    int32_t cur_count = ecs_query_table_count(query);

    if (prev_count != cur_count) {
        ecs_query_table_t *qt = ecs_table_cache_get(&query->cache, table);
        ecs_assert(qt != NULL, ECS_INTERNAL_ERROR, NULL);
        ecs_query_table_match_t *cur, *next;

        for (cur = qt->first; cur != NULL; cur = next) {
            next = cur->next_match;

            if (empty) {
                ecs_assert(ecs_table_count(table) == 0, 
                    ECS_INTERNAL_ERROR, NULL);

                flecs_query_remove_table_node(query, cur);
            } else {
                ecs_assert(ecs_table_count(table) != 0, 
                    ECS_INTERNAL_ERROR, NULL);
                flecs_query_insert_table_node(query, cur);
            }
        }
    }

    ecs_assert(cur_count || query->list.first == NULL, 
        ECS_INTERNAL_ERROR, NULL);
}

static
void flecs_query_add_subquery(
    ecs_world_t *world, 
    ecs_query_t *parent, 
    ecs_query_t *subquery) 
{
    ecs_vec_init_if_t(&parent->subqueries, ecs_query_t*);
    ecs_query_t **elem = ecs_vec_append_t(
        NULL, &parent->subqueries, ecs_query_t*);
    *elem = subquery;

    ecs_table_cache_t *cache = &parent->cache;
    ecs_table_cache_iter_t it;
    ecs_query_table_t *qt;
    flecs_table_cache_all_iter(cache, &it);
    while ((qt = flecs_table_cache_next(&it, ecs_query_table_t))) {
        flecs_query_match_table(world, subquery, qt->hdr.table);
    }
}

static
void flecs_query_notify_subqueries(
    ecs_world_t *world,
    ecs_query_t *query,
    ecs_query_event_t *event)
{
    if (query->subqueries.array) {
        ecs_query_t **queries = ecs_vec_first(&query->subqueries);
        int32_t i, count = ecs_vec_count(&query->subqueries);

        ecs_query_event_t sub_event = *event;
        sub_event.parent_query = query;

        for (i = 0; i < count; i ++) {
            ecs_query_t *sub = queries[i];
            flecs_query_notify(world, sub, &sub_event);
        }
    }
}

/* Remove table */
static
void flecs_query_table_match_free(
    ecs_query_t *query,
    ecs_query_table_t *elem,
    ecs_query_table_match_t *first)
{
    ecs_query_table_match_t *cur, *next;
    ecs_world_t *world = query->filter.world;

    for (cur = first; cur != NULL; cur = next) {
        flecs_bfree(&query->allocators.columns, cur->columns);
        flecs_bfree(&query->allocators.columns, cur->storage_columns);
        flecs_bfree(&query->allocators.ids, cur->ids);
        flecs_bfree(&query->allocators.sources, cur->sources);

        if (cur->monitor) {
            flecs_bfree(&query->allocators.monitors, cur->monitor);
        }
        if (!elem->hdr.empty) {
            flecs_query_remove_table_node(query, cur);
        }
        
        ecs_vec_fini_t(&world->allocator, &cur->refs, ecs_ref_t);
        flecs_entity_filter_fini(world, cur->entity_filter);

        next = cur->next_match;

        flecs_bfree(&world->allocators.query_table_match, cur);
    }
}

static
void flecs_query_table_free(
    ecs_query_t *query,
    ecs_query_table_t *elem)
{
    flecs_query_table_match_free(query, elem, elem->first);
    flecs_bfree(&query->filter.world->allocators.query_table, elem);
}

static
void flecs_query_unmatch_table(
    ecs_query_t *query,
    ecs_table_t *table,
    ecs_query_table_t *elem)
{
    if (!elem) {
        elem = ecs_table_cache_get(&query->cache, table);
    }
    if (elem) {
        ecs_table_cache_remove(&query->cache, elem->table_id, &elem->hdr);
        flecs_query_table_free(query, elem);
    }
}

/* Rematch system with tables after a change happened to a watched entity */
static
void flecs_query_rematch_tables(
    ecs_world_t *world,
    ecs_query_t *query,
    ecs_query_t *parent_query)
{
    ecs_iter_t it, parent_it;
    ecs_table_t *table = NULL;
    ecs_query_table_t *qt = NULL;
    ecs_query_table_match_t *qm = NULL;

    if (query->monitor_generation == world->monitor_generation) {
        return;
    }

    query->monitor_generation = world->monitor_generation;

    if (parent_query) {
        parent_it = ecs_query_iter(world, parent_query);
        it = ecs_filter_chain_iter(&parent_it, &query->filter);
    } else {
        it = ecs_filter_iter(world, &query->filter);
    }

    ECS_BIT_SET(it.flags, EcsIterIsInstanced);
    ECS_BIT_SET(it.flags, EcsIterNoData);
    ECS_BIT_SET(it.flags, EcsIterEntityOptional);

    world->info.rematch_count_total ++;
    int32_t rematch_count = ++ query->rematch_count;

    ecs_time_t t = {0};
    if (world->flags & EcsWorldMeasureFrameTime) {
        ecs_time_measure(&t);
    }

    while (ecs_filter_next(&it)) {
        if ((table != it.table) || (!it.table && !qt)) {
            if (qm && qm->next_match) {
                flecs_query_table_match_free(query, qt, qm->next_match);
                qm->next_match = NULL;
            }

            table = it.table;

            qt = ecs_table_cache_get(&query->cache, table);
            if (!qt) {
                qt = flecs_query_table_insert(world, query, table);
            }

            ecs_assert(qt->hdr.table == table, ECS_INTERNAL_ERROR, NULL);
            qt->rematch_count = rematch_count;
            qm = NULL;
        }
        if (!qm) {
            qm = qt->first;
        } else {
            qm = qm->next_match;
        }
        if (!qm) {
            qm = flecs_query_add_table_match(query, qt, table);
        }

        flecs_query_set_table_match(world, query, qm, table, &it);

        if (table && ecs_table_count(table) && query->group_by) {
            if (flecs_query_get_group_id(query, table) != qm->group_id) {
                /* Update table group */
                flecs_query_remove_table_node(query, qm);
                flecs_query_insert_table_node(query, qm);
            }
        }
    }

    if (qm && qm->next_match) {
        flecs_query_table_match_free(query, qt, qm->next_match);
        qm->next_match = NULL;
    }

    /* Iterate all tables in cache, remove ones that weren't just matched */
    ecs_table_cache_iter_t cache_it;
    if (flecs_table_cache_all_iter(&query->cache, &cache_it)) {
        while ((qt = flecs_table_cache_next(&cache_it, ecs_query_table_t))) {
            if (qt->rematch_count != rematch_count) {
                flecs_query_unmatch_table(query, qt->hdr.table, qt);
            }
        }
    }

    if (world->flags & EcsWorldMeasureFrameTime) {
        world->info.rematch_time_total += (ecs_ftime_t)ecs_time_measure(&t);
    }
}

static
void flecs_query_remove_subquery(
    ecs_query_t *parent, 
    ecs_query_t *sub)
{
    ecs_assert(parent != NULL, ECS_INTERNAL_ERROR, NULL);
    ecs_assert(sub != NULL, ECS_INTERNAL_ERROR, NULL);
    ecs_assert(parent->subqueries.array, ECS_INTERNAL_ERROR, NULL);

    int32_t i, count = ecs_vec_count(&parent->subqueries);
    ecs_query_t **sq = ecs_vec_first(&parent->subqueries);

    for (i = 0; i < count; i ++) {
        if (sq[i] == sub) {
            break;
        }
    }

    ecs_vec_remove_t(&parent->subqueries, ecs_query_t*, i);
}

/* -- Private API -- */

void flecs_query_notify(
    ecs_world_t *world,
    ecs_query_t *query,
    ecs_query_event_t *event)
{
    bool notify = true;

    switch(event->kind) {
    case EcsQueryTableMatch:
        /* Creation of new table */
        if (flecs_query_match_table(world, query, event->table)) {
            if (query->subqueries.array) {
                flecs_query_notify_subqueries(world, query, event);
            }
        }
        notify = false;
        break;
    case EcsQueryTableUnmatch:
        /* Deletion of table */
        flecs_query_unmatch_table(query, event->table, NULL);
        break;
    case EcsQueryTableRematch:
        /* Rematch tables of query */
        flecs_query_rematch_tables(world, query, event->parent_query);
        break;        
    case EcsQueryOrphan:
        ecs_assert(query->flags & EcsQueryIsSubquery, ECS_INTERNAL_ERROR, NULL);
        query->flags |= EcsQueryIsOrphaned;
        query->parent = NULL;
        break;
    }

    if (notify) {
        flecs_query_notify_subqueries(world, query, event);
    }
}

static
void flecs_query_order_by(
    ecs_world_t *world,
    ecs_query_t *query,
    ecs_entity_t order_by_component,
    ecs_order_by_action_t order_by,
    ecs_sort_table_action_t action)
{
    ecs_check(query != NULL, ECS_INVALID_PARAMETER, NULL);
    ecs_check(!(query->flags & EcsQueryIsOrphaned), ECS_INVALID_PARAMETER, NULL);
    ecs_check(!ecs_id_is_wildcard(order_by_component), 
        ECS_INVALID_PARAMETER, NULL);

    /* Find order_by_component term & make sure it is queried for */
    const ecs_filter_t *filter = &query->filter;
    int32_t i, count = filter->term_count;
    int32_t order_by_term = -1;

    if (order_by_component) {
        for (i = 0; i < count; i ++) {
            ecs_term_t *term = &filter->terms[i];
            
            /* Only And terms are supported */
            if (term->id == order_by_component && term->oper == EcsAnd) {
                order_by_term = i;
                break;
            }
        }

        ecs_check(order_by_term != -1, ECS_INVALID_PARAMETER, 
            "sorted component not is queried for");
    }

    query->order_by_component = order_by_component;
    query->order_by = order_by;
    query->order_by_term = order_by_term;
    query->sort_table = action;

    ecs_vec_fini_t(NULL, &query->table_slices, ecs_query_table_match_t);
    flecs_query_sort_tables(world, query);  

    if (!query->table_slices.array) {
        flecs_query_build_sorted_tables(query);
    }

    query->flags &= ~EcsQueryTrivialIter;
error:
    return;
}

static
void flecs_query_group_by(
    ecs_query_t *query,
    ecs_entity_t sort_component,
    ecs_group_by_action_t group_by)
{   
    /* Cannot change grouping once a query has been created */
    ecs_check(query->group_by_id == 0, ECS_INVALID_OPERATION, NULL);
    ecs_check(query->group_by == 0, ECS_INVALID_OPERATION, NULL);

    if (!group_by) {
        /* Builtin function that groups by relationship */
        group_by = flecs_query_default_group_by;   
    }

    query->group_by_id = sort_component;
    query->group_by = group_by;

    ecs_map_init_w_params(&query->groups, 
        &query->filter.world->allocators.query_table_list);
error:
    return;
}

/* Implementation for iterable mixin */
static
void flecs_query_iter_init(
    const ecs_world_t *world,
    const ecs_poly_t *poly,
    ecs_iter_t *iter,
    ecs_term_t *filter)
{
    ecs_poly_assert(poly, ecs_query_t);

    if (filter) {
        iter[1] = ecs_query_iter(world, (ecs_query_t*)poly);
        iter[0] = ecs_term_chain_iter(&iter[1], filter);
    } else {
        iter[0] = ecs_query_iter(world, (ecs_query_t*)poly);
    }
}

static
void flecs_query_on_event(
    ecs_iter_t *it)
{
    /* Because this is the observer::run callback, checking if this is event is
     * already handled is not done for us. */
    ecs_world_t *world = it->world;
    ecs_observer_t *o = it->ctx;
    if (o->last_event_id) {
        if (o->last_event_id[0] == world->event_id) {
            return;
        }
        o->last_event_id[0] = world->event_id;
    }

    ecs_query_t *query = o->ctx;
    ecs_table_t *table = it->table;
    ecs_entity_t event = it->event;

    if (event == EcsOnTableCreate) {
        /* Creation of new table */
        if (flecs_query_match_table(world, query, table)) {
            if (query->subqueries.array) {
                ecs_query_event_t evt = {
                    .kind = EcsQueryTableMatch,
                    .table = table,
                    .parent_query = query
                };
                flecs_query_notify_subqueries(world, query, &evt);
            }
        }
        return;
    }

    ecs_assert(query != NULL, ECS_INTERNAL_ERROR, NULL);

    /* The observer isn't doing the matching because the query can do it more
     * efficiently by checking the table with the query cache. */
    if (ecs_table_cache_get(&query->cache, table) == NULL) {
        return;
    }

    if (event == EcsOnTableEmpty) {
        flecs_query_update_table(query, table, true);
    } else
    if (event == EcsOnTableFill) {
        flecs_query_update_table(query, table, false);
    } else if (event == EcsOnTableDelete) {
        /* Deletion of table */
        flecs_query_unmatch_table(query, table, NULL);
        if (query->subqueries.array) {
                ecs_query_event_t evt = {
                    .kind = EcsQueryTableUnmatch,
                    .table = table,
                    .parent_query = query
                };
                flecs_query_notify_subqueries(world, query, &evt);
        }
        return;
    }
}

static
void flecs_query_table_cache_free(
    ecs_query_t *query)
{
    ecs_table_cache_iter_t it;
    ecs_query_table_t *qt;

    if (flecs_table_cache_all_iter(&query->cache, &it)) {
        while ((qt = flecs_table_cache_next(&it, ecs_query_table_t))) {
            flecs_query_table_free(query, qt);
        }
    }

    ecs_table_cache_fini(&query->cache);
}

static
void flecs_query_allocators_init(
    ecs_query_t *query)
{
    int32_t field_count = query->filter.field_count;
    if (field_count) {
        flecs_ballocator_init(&query->allocators.columns, 
            field_count * ECS_SIZEOF(int32_t));
        flecs_ballocator_init(&query->allocators.ids, 
            field_count * ECS_SIZEOF(ecs_id_t));
        flecs_ballocator_init(&query->allocators.sources,
            field_count * ECS_SIZEOF(ecs_entity_t));
        flecs_ballocator_init(&query->allocators.monitors,
            (1 + field_count) * ECS_SIZEOF(int32_t));
    }
}

static
void flecs_query_allocators_fini(
    ecs_query_t *query)
{
    int32_t field_count = query->filter.field_count;
    if (field_count) {
        flecs_ballocator_fini(&query->allocators.columns);
        flecs_ballocator_fini(&query->allocators.ids);
        flecs_ballocator_fini(&query->allocators.sources);
        flecs_ballocator_fini(&query->allocators.monitors);
    }
}

static
void flecs_query_fini(
    ecs_query_t *query)
{
    ecs_world_t *world = query->filter.world;

    ecs_group_delete_action_t on_delete = query->on_group_delete;
    if (on_delete) {
        ecs_map_iter_t it = ecs_map_iter(&query->groups);
        while (ecs_map_next(&it)) {
            ecs_query_table_list_t *group = ecs_map_ptr(&it);
            uint64_t group_id = ecs_map_key(&it);
            on_delete(world, group_id, group->info.ctx, query->group_by_ctx);
        }
        query->on_group_delete = NULL;
    }

    if (query->group_by_ctx_free) {
        if (query->group_by_ctx) {
            query->group_by_ctx_free(query->group_by_ctx);
        }
    }

    if ((query->flags & EcsQueryIsSubquery) &&
        !(query->flags & EcsQueryIsOrphaned))
    {
        flecs_query_remove_subquery(query->parent, query);
    }

    flecs_query_notify_subqueries(world, query, &(ecs_query_event_t){
        .kind = EcsQueryOrphan
    });

    flecs_query_for_each_component_monitor(world, query, 
        flecs_monitor_unregister);
    flecs_query_table_cache_free(query);

    ecs_map_fini(&query->groups);

    ecs_vec_fini_t(NULL, &query->subqueries, ecs_query_t*);
    ecs_vec_fini_t(NULL, &query->table_slices, ecs_query_table_match_t);
    ecs_filter_fini(&query->filter);

    flecs_query_allocators_fini(query);

    ecs_poly_free(query, ecs_query_t);
}

/* -- Public API -- */

ecs_query_t* ecs_query_init(
    ecs_world_t *world,
    const ecs_query_desc_t *desc)
{
    ecs_check(world != NULL, ECS_INTERNAL_ERROR, NULL);
    ecs_check(desc != NULL, ECS_INVALID_PARAMETER, NULL);
    ecs_check(desc->_canary == 0, ECS_INVALID_PARAMETER, NULL);
    ecs_check(!(world->flags & EcsWorldFini), ECS_INVALID_OPERATION, NULL);

    ecs_query_t *result = ecs_poly_new(ecs_query_t);
    ecs_observer_desc_t observer_desc = { .filter = desc->filter };
    ecs_entity_t entity = desc->filter.entity;

    observer_desc.filter.flags = EcsFilterMatchEmptyTables;
    observer_desc.filter.storage = &result->filter;
    result->filter = ECS_FILTER_INIT;

    if (ecs_filter_init(world, &observer_desc.filter) == NULL) {
        goto error;
    }

    ECS_BIT_COND(result->flags, EcsQueryTrivialIter, 
        !!(result->filter.flags & EcsFilterMatchOnlyThis));

    flecs_query_allocators_init(result);

    if (result->filter.term_count) {
        observer_desc.entity = entity;
        observer_desc.run = flecs_query_on_event;
        observer_desc.ctx = result;
        observer_desc.events[0] = EcsOnTableEmpty;
        observer_desc.events[1] = EcsOnTableFill;
        if (!desc->parent) {
            observer_desc.events[2] = EcsOnTableCreate;
            observer_desc.events[3] = EcsOnTableDelete;
        }
        observer_desc.filter.flags |= EcsFilterNoData;
        observer_desc.filter.instanced = true;

        /* ecs_filter_init could have moved away resources from the terms array
         * in the descriptor, so use the terms array from the filter. */
        observer_desc.filter.terms_buffer = result->filter.terms;
        observer_desc.filter.terms_buffer_count = result->filter.term_count;
        observer_desc.filter.expr = NULL; /* Already parsed */

        entity = ecs_observer_init(world, &observer_desc);
        if (!entity) {
            goto error;
        }
    }

    result->iterable.init = flecs_query_iter_init;
    result->dtor = (ecs_poly_dtor_t)flecs_query_fini;
    result->prev_match_count = -1;

    if (ecs_should_log_1()) {
        char *filter_expr = ecs_filter_str(world, &result->filter);
        ecs_dbg_1("#[green]query#[normal] [%s] created", 
            filter_expr ? filter_expr : "");
        ecs_os_free(filter_expr);
    }

    ecs_log_push_1();

    if (flecs_query_process_signature(world, result)) {
        goto error;
    }

    /* Group before matching so we won't have to move tables around later */
    int32_t cascade_by = result->cascade_by;
    if (cascade_by) {
        flecs_query_group_by(result, result->filter.terms[cascade_by - 1].id,
            flecs_query_group_by_cascade);
        result->group_by_ctx = &result->filter.terms[cascade_by - 1];
    }

    if (desc->group_by || desc->group_by_id) {
        /* Can't have a cascade term and group by at the same time, as cascade
         * uses the group_by mechanism */
        ecs_check(!result->cascade_by, ECS_INVALID_PARAMETER, NULL);
        flecs_query_group_by(result, desc->group_by_id, desc->group_by);
        result->group_by_ctx = desc->group_by_ctx;
        result->on_group_create = desc->on_group_create;
        result->on_group_delete = desc->on_group_delete;
        result->group_by_ctx_free = desc->group_by_ctx_free;
    }

    if (desc->parent != NULL) {
        result->flags |= EcsQueryIsSubquery;
    }

    /* If the query refers to itself, add the components that were queried for
     * to the query itself. */
    if (entity)  {
        int32_t t, term_count = result->filter.term_count;
        ecs_term_t *terms = result->filter.terms;

        for (t = 0; t < term_count; t ++) {
            ecs_term_t *term = &terms[t];
            if (term->src.id == entity) {
                ecs_add_id(world, entity, term->id);
            }
        }        
    }

    if (!entity) {
        entity = ecs_new_id(world);
    }

    EcsPoly *poly = ecs_poly_bind(world, entity, ecs_query_t);
    if (poly->poly) {
        /* If entity already had poly query, delete previous */
        flecs_query_fini(poly->poly);
    }
    poly->poly = result;
    result->filter.entity = entity;

    /* Ensure that while initially populating the query with tables, they are
     * in the right empty/non-empty list. This ensures the query won't miss
     * empty/non-empty events for tables that are currently out of sync, but
     * change back to being in sync before processing pending events. */
    ecs_run_aperiodic(world, EcsAperiodicEmptyTables);

    ecs_table_cache_init(world, &result->cache);

    if (!desc->parent) {
        flecs_query_match_tables(world, result);
    } else {
        flecs_query_add_subquery(world, desc->parent, result);
        result->parent = desc->parent;
    }

    if (desc->order_by) {
        flecs_query_order_by(
            world, result, desc->order_by_component, desc->order_by,
            desc->sort_table);
    }

    if (!ecs_query_table_count(result) && result->filter.term_count) {
        ecs_add_id(world, entity, EcsEmpty);
    }

    ecs_poly_modified(world, entity, ecs_query_t);

    ecs_log_pop_1();

    return result;
error:
    if (result) {
        ecs_filter_fini(&result->filter);
        ecs_os_free(result);
    }
    return NULL;
}

void ecs_query_fini(
    ecs_query_t *query)
{
    ecs_poly_assert(query, ecs_query_t);
    ecs_delete(query->filter.world, query->filter.entity);
}

const ecs_filter_t* ecs_query_get_filter(
    const ecs_query_t *query)
{
    ecs_poly_assert(query, ecs_query_t);
    return &query->filter;
}

ecs_iter_t ecs_query_iter(
    const ecs_world_t *stage,
    ecs_query_t *query)
{
    ecs_poly_assert(query, ecs_query_t);
    ecs_check(!(query->flags & EcsQueryIsOrphaned),
        ECS_INVALID_PARAMETER, NULL);

    ecs_world_t *world = query->filter.world;
    ecs_poly_assert(world, ecs_world_t);

    /* Process table events to ensure that the list of iterated tables doesn't
     * contain empty tables. */
    flecs_process_pending_tables(world);

    /* If query has order_by, apply sort */
    flecs_query_sort_tables(world, query);

    /* If monitors changed, do query rematching */
    if (!(world->flags & EcsWorldReadonly) && query->flags & EcsQueryHasRefs) {
        flecs_eval_component_monitors(world);
    }

    /* Prepare iterator */

    int32_t table_count;
    if (ecs_vec_count(&query->table_slices)) {
        table_count = ecs_vec_count(&query->table_slices);
    } else {
        table_count = ecs_query_table_count(query);
    }

    ecs_query_iter_t it = {
        .query = query,
        .node = query->list.first,
        .last = NULL
    };

    if (query->order_by && query->list.info.table_count) {
        it.node = ecs_vec_first(&query->table_slices);
    }

    ecs_iter_t result = {
        .real_world = world,
        .world = (ecs_world_t*)stage,
        .terms = query->filter.terms,
        .field_count = query->filter.field_count,
        .table_count = table_count,
        .priv.iter.query = it,
        .next = ecs_query_next,
    };

    flecs_filter_apply_iter_flags(&result, &query->filter);

    ecs_filter_t *filter = &query->filter;
    ecs_iter_t fit;
    if (!(query->flags & EcsQueryTrivialIter)) {
        /* Check if non-This terms (like singleton terms) still match */
        if (!(filter->flags & EcsFilterMatchOnlyThis)) {
            fit = flecs_filter_iter_w_flags(
                (ecs_world_t*)stage, &query->filter, EcsIterIgnoreThis);
            if (!ecs_filter_next(&fit)) {
                /* No match, so return nothing */
                ecs_iter_fini(&fit);
                goto noresults;
            }
        }

        flecs_iter_init(stage, &result, flecs_iter_cache_all);

        /* Copy the data */
        if (!(filter->flags & EcsFilterMatchOnlyThis)) {
            int32_t field_count = filter->field_count;
            if (field_count) {
                if (result.ptrs) {
                    ecs_os_memcpy_n(result.ptrs, fit.ptrs, void*, field_count);
                }
                ecs_os_memcpy_n(result.ids, fit.ids, ecs_id_t, field_count);
                ecs_os_memcpy_n(result.columns, fit.columns, int32_t, field_count);
                ecs_os_memcpy_n(result.sources, fit.sources, int32_t, field_count);
            }

            ecs_iter_fini(&fit);
        }
    } else {
        /* Trivial iteration, use arrays from query cache */
        flecs_iter_init(stage, &result, flecs_iter_cache_ptrs);
    }

    result.sizes = query->filter.sizes;

    return result;
error:
noresults:
    result.priv.iter.query.node = NULL;
    return result;
}

void ecs_query_set_group(
    ecs_iter_t *it,
    uint64_t group_id)
{
    ecs_check(it != NULL, ECS_INVALID_PARAMETER, NULL);
    ecs_check(it->next == ecs_query_next, ECS_INVALID_PARAMETER, NULL);
    ecs_check(!(it->flags & EcsIterIsValid), ECS_INVALID_PARAMETER, NULL);

    ecs_query_iter_t *qit = &it->priv.iter.query;
    ecs_query_t *q = qit->query;
    ecs_check(q != NULL, ECS_INVALID_PARAMETER, NULL);

    ecs_query_table_list_t *node = flecs_query_get_group(q, group_id);
    if (!node) {
        qit->node = NULL;
        return;
    }

    ecs_query_table_match_t *first = node->first;
    if (first) {
        qit->node = node->first;
        qit->last = node->last->next;
    } else {
        qit->node = NULL;
        qit->last = NULL;
    }
    
error:
    return;
}

const ecs_query_group_info_t* ecs_query_get_group_info(
    const ecs_query_t *query,
    uint64_t group_id)
{
    ecs_query_table_list_t *node = flecs_query_get_group(query, group_id);
    if (!node) {
        return NULL;
    }
    
    return &node->info;
}

void* ecs_query_get_group_ctx(
    const ecs_query_t *query,
    uint64_t group_id)
{
    const ecs_query_group_info_t *info = 
        ecs_query_get_group_info(query, group_id);
    if (!info) {
        return NULL;
    } else {
        return info->ctx;
    }
}

static
void flecs_query_mark_columns_dirty(
    ecs_query_t *query,
    ecs_query_table_match_t *qm)
{
    ecs_table_t *table = qm->table;
    if (!table) {
        return;
    }

    int32_t *dirty_state = table->dirty_state;
    if (dirty_state) {
        int32_t *storage_columns = qm->storage_columns;
        ecs_filter_t *filter = &query->filter;
        ecs_term_t *terms = filter->terms;
        int32_t i, count = filter->term_count;

        for (i = 0; i < count; i ++) {
            ecs_term_t *term = &terms[i];
            if (term->inout == EcsIn || term->inout == EcsInOutNone) {
                /* Don't mark readonly terms dirty */
                continue;
            }

            int32_t field = term->field_index;
            int32_t column = storage_columns[field];
            if (column < 0) {
                continue;
            }

            dirty_state[column + 1] ++;
        }
    }
}

bool ecs_query_next_table(
    ecs_iter_t *it)
{
    ecs_check(it != NULL, ECS_INVALID_PARAMETER, NULL);
    ecs_check(it->next == ecs_query_next, ECS_INVALID_PARAMETER, NULL);

    flecs_iter_validate(it);

    ecs_query_iter_t *iter = &it->priv.iter.query;
    ecs_query_table_match_t *node = iter->node;
    ecs_query_t *query = iter->query;

    ecs_query_table_match_t *prev = iter->prev;
    if (prev) {
        if (query->flags & EcsQueryHasMonitor) {
            flecs_query_sync_match_monitor(query, prev);
        }
        if (query->flags & EcsQueryHasOutColumns) {
            if (it->count) {
                flecs_query_mark_columns_dirty(query, prev);
            }
        }
    }

    if (node != iter->last) {
        it->table = node->table;
        it->group_id = node->group_id;
        it->count = 0;
        iter->node = node->next;
        iter->prev = node;
        return true;
    }

error:
    query->match_count = query->prev_match_count;
    ecs_iter_fini(it);
    return false;
}

static
void flecs_query_populate_trivial(
    ecs_iter_t *it,
    ecs_query_table_match_t *match)
{;
    ecs_table_t *table = match->table;
    int32_t count = ecs_table_count(table);

    it->ids = match->ids;
    it->sources = match->sources;
    it->columns = match->columns;
    it->group_id = match->group_id;
    it->instance_count = 0;
    it->offset = 0;
    it->count = count;
    it->references = ecs_vec_first(&match->refs);

    if (!it->references) {
        ecs_data_t *data = &table->data;
        if (!(it->flags & EcsIterNoData)) {
            int32_t i;
            for (i = 0; i < it->field_count; i ++) {
                int32_t column = match->storage_columns[i];
                if (column < 0) {
                    it->ptrs[i] = NULL;
                    continue;
                }

                ecs_size_t size = it->sizes[i];
                if (!size) {
                    it->ptrs[i] = NULL;
                    continue;
                }

                it->ptrs[i] = ecs_vec_get(&data->columns[column], 
                    it->sizes[i], 0);
            }
        }

        it->frame_offset += it->table ? ecs_table_count(it->table) : 0;
        it->table = table;
        it->entities = ecs_vec_first(&data->entities);
    } else {
        flecs_iter_populate_data(it->real_world, it, table, 0, count, it->ptrs);
    }
}

int ecs_query_populate(
    ecs_iter_t *it,
    bool when_changed)
{
    ecs_check(it != NULL, ECS_INVALID_PARAMETER, NULL);
    ecs_check(it->next == ecs_query_next, ECS_INVALID_PARAMETER, NULL);
    ecs_check(ECS_BIT_IS_SET(it->flags, EcsIterIsValid), 
        ECS_INVALID_PARAMETER, NULL);

    ecs_query_iter_t *iter = &it->priv.iter.query;
    ecs_query_t *query = iter->query;
    ecs_query_table_match_t *match = iter->prev;
    ecs_assert(match != NULL, ECS_INVALID_OPERATION, NULL);
    if (query->flags & EcsQueryTrivialIter) {
        flecs_query_populate_trivial(it, match);
        return EcsIterNextYield;
    }

    ecs_table_t *table = match->table;
    ecs_world_t *world = query->filter.world;
    const ecs_filter_t *filter = &query->filter;
    ecs_entity_filter_iter_t *ent_it = it->priv.entity_iter;
    ecs_assert(ent_it != NULL, ECS_INTERNAL_ERROR, NULL);
    ecs_table_range_t *range = &ent_it->range;
    int32_t t, term_count = filter->term_count;
    int result;

repeat:
    result = EcsIterNextYield;

    ecs_os_memcpy_n(it->sources, match->sources, ecs_entity_t, 
        filter->field_count);

    for (t = 0; t < term_count; t ++) {
        ecs_term_t *term = &filter->terms[t];
        int32_t field = term->field_index;
        if (!ecs_term_match_this(term)) {
            continue;
        }

        it->ids[field] = match->ids[field];
        it->columns[field] = match->columns[field];
    }

    if (table) {
        range->offset = match->offset;
        range->count = match->count;
        if (!range->count) {
            range->count = ecs_table_count(table);
            ecs_assert(range->count != 0, ECS_INTERNAL_ERROR, NULL);
        }

        if (match->entity_filter) {
            ent_it->entity_filter = match->entity_filter;
            ent_it->columns = match->columns;
            ent_it->range.table = table;
            ent_it->it = it;
            result = flecs_entity_filter_next(ent_it);
            if (result == EcsIterNext) {
                goto done;
            }
        }

        it->group_id = match->group_id;
    } else {
        range->offset = 0;
        range->count = 0;
    }

    if (when_changed) {
        if (!ecs_query_changed(NULL, it)) {
            if (result == EcsIterYield) {
                goto repeat;
            } else {
                result = EcsIterNext;
                goto done;
            }
        }
    }

    it->references = ecs_vec_first(&match->refs);
    it->instance_count = 0;

    flecs_iter_populate_data(world, it, table, range->offset, range->count,
        it->ptrs);

error:
done:
    return result;
}

bool ecs_query_next(
    ecs_iter_t *it)
{
    ecs_check(it != NULL, ECS_INVALID_PARAMETER, NULL);
    ecs_check(it->next == ecs_query_next, ECS_INVALID_PARAMETER, NULL);

    if (flecs_iter_next_row(it)) {
        return true;
    }

    return flecs_iter_next_instanced(it, ecs_query_next_instanced(it));
error:
    return false;
}

bool ecs_query_next_instanced(
    ecs_iter_t *it)
{
    ecs_check(it != NULL, ECS_INVALID_PARAMETER, NULL);
    ecs_check(it->next == ecs_query_next, ECS_INVALID_PARAMETER, NULL);

    ecs_query_iter_t *iter = &it->priv.iter.query;
    ecs_query_t *query = iter->query;
    ecs_flags32_t flags = query->flags;

    ecs_query_table_match_t *prev, *next, *cur = iter->node, *last = iter->last;
    if ((prev = iter->prev)) {
        /* Match has been iterated, update monitor for change tracking */
        if (flags & EcsQueryHasMonitor) {
            flecs_query_sync_match_monitor(query, prev);
        }
        if (flags & EcsQueryHasOutColumns) {
            flecs_query_mark_columns_dirty(query, prev);
        }
    }

    flecs_iter_validate(it);
    iter->skip_count = 0;

    /* Trivial iteration: each entry in the cache is a full match and ids are
     * only matched on $this or through traversal starting from $this. */
    if (flags & EcsQueryTrivialIter) {
        if (cur == last) {
            goto done;
        }
        iter->node = cur->next;
        iter->prev = cur;
        flecs_query_populate_trivial(it, cur);
        return true;
    }

    /* Non-trivial iteration: query matches with static sources, or matches with
     * tables that require per-entity filtering. */
    for (; cur != last; cur = next) {
        next = cur->next;
        iter->prev = cur;
        switch(ecs_query_populate(it, false)) {
        case EcsIterNext: iter->node = next; continue;
        case EcsIterYield: next = cur; /* fall through */
        case EcsIterNextYield: goto yield;
        default: ecs_abort(ECS_INTERNAL_ERROR, NULL);
        }
    }

done: error:
    query->match_count = query->prev_match_count;
    ecs_iter_fini(it);
    return false;

yield:
    iter->node = next;
    iter->prev = cur;
    return true;
}

bool ecs_query_changed(
    ecs_query_t *query,
    const ecs_iter_t *it)
{
    if (it) {
        ecs_check(it->next == ecs_query_next, ECS_INVALID_PARAMETER, NULL);
        ecs_check(ECS_BIT_IS_SET(it->flags, EcsIterIsValid), 
            ECS_INVALID_PARAMETER, NULL);

        ecs_query_table_match_t *qm = 
            (ecs_query_table_match_t*)it->priv.iter.query.prev;
        ecs_assert(qm != NULL, ECS_INVALID_PARAMETER, NULL);

        if (!query) {
            query = it->priv.iter.query.query;
        } else {
            ecs_check(query == it->priv.iter.query.query, 
                ECS_INVALID_PARAMETER, NULL);
        }

        ecs_check(query != NULL, ECS_INVALID_PARAMETER, NULL);
        ecs_poly_assert(query, ecs_query_t);

        return flecs_query_check_match_monitor(query, qm, it);
    }

    ecs_poly_assert(query, ecs_query_t);
    ecs_check(!(query->flags & EcsQueryIsOrphaned), 
        ECS_INVALID_PARAMETER, NULL);

    flecs_process_pending_tables(query->filter.world);

    if (!(query->flags & EcsQueryHasMonitor)) {
        query->flags |= EcsQueryHasMonitor;
        flecs_query_init_query_monitors(query);
        return true; /* Monitors didn't exist yet */
    }

    if (query->match_count != query->prev_match_count) {
        return true;
    }

    return flecs_query_check_query_monitor(query);
error:
    return false;
}

void ecs_query_skip(
    ecs_iter_t *it)
{
    ecs_assert(it->next == ecs_query_next, ECS_INVALID_PARAMETER, NULL);
    ecs_assert(ECS_BIT_IS_SET(it->flags, EcsIterIsValid), 
        ECS_INVALID_PARAMETER, NULL);

    if (it->instance_count > it->count) {
        it->priv.iter.query.skip_count ++;
        if (it->priv.iter.query.skip_count == it->instance_count) {
            /* For non-instanced queries, make sure all entities are skipped */
            it->priv.iter.query.prev = NULL;
        }
    } else {
        it->priv.iter.query.prev = NULL;
    }
}

bool ecs_query_orphaned(
    const ecs_query_t *query)
{
    ecs_poly_assert(query, ecs_query_t);
    return query->flags & EcsQueryIsOrphaned;
}

char* ecs_query_str(
    const ecs_query_t *query)
{
    return ecs_filter_str(query->filter.world, &query->filter);
}

int32_t ecs_query_table_count(
    const ecs_query_t *query)
{
    ecs_run_aperiodic(query->filter.world, EcsAperiodicEmptyTables);
    return query->cache.tables.count;
}

int32_t ecs_query_empty_table_count(
    const ecs_query_t *query)
{
    ecs_run_aperiodic(query->filter.world, EcsAperiodicEmptyTables);
    return query->cache.empty_tables.count;
}

int32_t ecs_query_entity_count(
    const ecs_query_t *query)
{
    ecs_run_aperiodic(query->filter.world, EcsAperiodicEmptyTables);
    
    int32_t result = 0;
    ecs_table_cache_hdr_t *cur, *last = query->cache.tables.last;
    if (!last) {
        return 0;
    }

    for (cur = query->cache.tables.first; cur != NULL; cur = cur->next) {
        result += ecs_table_count(cur->table);
    }

    return result;
}

/**
 * @file table_graph.c
 * @brief Data structure to speed up table transitions.
 * 
 * The table graph is used to speed up finding tables in add/remove operations.
 * For example, if component C is added to an entity in table [A, B], the entity
 * must be moved to table [A, B, C]. The graph speeds this process up with an
 * edge for component C that connects [A, B] to [A, B, C].
 */


/* Id sequence (type) utilities */

static
uint64_t flecs_type_hash(const void *ptr) {
    const ecs_type_t *type = ptr;
    ecs_id_t *ids = type->array;
    int32_t count = type->count;
    return flecs_hash(ids, count * ECS_SIZEOF(ecs_id_t));
}

static
int flecs_type_compare(const void *ptr_1, const void *ptr_2) {
    const ecs_type_t *type_1 = ptr_1;
    const ecs_type_t *type_2 = ptr_2;

    int32_t count_1 = type_1->count;
    int32_t count_2 = type_2->count;

    if (count_1 != count_2) {
        return (count_1 > count_2) - (count_1 < count_2);
    }

    const ecs_id_t *ids_1 = type_1->array;
    const ecs_id_t *ids_2 = type_2->array;
    int result = 0;
    
    int32_t i;
    for (i = 0; !result && (i < count_1); i ++) {
        ecs_id_t id_1 = ids_1[i];
        ecs_id_t id_2 = ids_2[i];
        result = (id_1 > id_2) - (id_1 < id_2);
    }

    return result;
}

void flecs_table_hashmap_init(
    ecs_world_t *world, 
    ecs_hashmap_t *hm) 
{
    flecs_hashmap_init(hm, ecs_type_t, ecs_table_t*, 
        flecs_type_hash, flecs_type_compare, &world->allocator);
}

/* Find location where to insert id into type */
static
int flecs_type_find_insert(
    const ecs_type_t *type,
    int32_t offset,
    ecs_id_t to_add)
{
    ecs_id_t *array = type->array;
    int32_t i, count = type->count;

    for (i = offset; i < count; i ++) {
        ecs_id_t id = array[i];
        if (id == to_add) {
            return -1;
        }
        if (id > to_add) {
            return i;
        }
    }
    return i;
}

/* Find location of id in type */
static
int flecs_type_find(
    const ecs_type_t *type,
    ecs_id_t id)
{
    ecs_id_t *array = type->array;
    int32_t i, count = type->count;

    for (i = 0; i < count; i ++) {
        ecs_id_t cur = array[i];
        if (ecs_id_match(cur, id)) {
            return i;
        }
        if (cur > id) {
            return -1;
        }
    }

    return -1;
}

/* Count number of matching ids */
static
int flecs_type_count_matches(
    const ecs_type_t *type,
    ecs_id_t wildcard,
    int32_t offset)
{
    ecs_id_t *array = type->array;
    int32_t i = offset, count = type->count;

    for (; i < count; i ++) {
        ecs_id_t cur = array[i];
        if (!ecs_id_match(cur, wildcard)) {
            break;
        }
    }

    return i - offset;
}

/* Create type from source type with id */
static
int flecs_type_new_with(
    ecs_world_t *world,
    ecs_type_t *dst,
    const ecs_type_t *src,
    ecs_id_t with)
{
    ecs_id_t *src_array = src->array;
    int32_t at = flecs_type_find_insert(src, 0, with);
    if (at == -1) {
        return -1;
    }

    int32_t dst_count = src->count + 1;
    ecs_id_t *dst_array = flecs_walloc_n(world, ecs_id_t, dst_count);
    dst->count = dst_count;
    dst->array = dst_array;

    if (at) {
        ecs_os_memcpy_n(dst_array, src_array, ecs_id_t, at);
    }

    int32_t remain = src->count - at;
    if (remain) {
        ecs_os_memcpy_n(&dst_array[at + 1], &src_array[at], ecs_id_t, remain);
    }

    dst_array[at] = with;

    return 0;
}

/* Create type from source type without ids matching wildcard */
static
int flecs_type_new_filtered(
    ecs_world_t *world,
    ecs_type_t *dst,
    const ecs_type_t *src,
    ecs_id_t wildcard,
    int32_t at)
{
    *dst = flecs_type_copy(world, src);
    ecs_id_t *dst_array = dst->array;
    ecs_id_t *src_array = src->array;
    if (at) {
        ecs_assert(dst_array != NULL, ECS_INTERNAL_ERROR, NULL);
        ecs_os_memcpy_n(dst_array, src_array, ecs_id_t, at);
    }

    int32_t i = at + 1, w = at, count = src->count;
    for (; i < count; i ++) {
        ecs_id_t id = src_array[i];
        if (!ecs_id_match(id, wildcard)) {
            dst_array[w] = id;
            w ++;
        }
    }

    dst->count = w;
    if (w != count) {
        dst->array = flecs_wrealloc_n(world, ecs_id_t, w, count, dst->array);
    }

    return 0;
}

/* Create type from source type without id */
static
int flecs_type_new_without(
    ecs_world_t *world,
    ecs_type_t *dst,
    const ecs_type_t *src,
    ecs_id_t without)
{
    ecs_id_t *src_array = src->array;
    int32_t count = 1, at = flecs_type_find(src, without);
    if (at == -1) {
        return -1;
    }

    int32_t src_count = src->count;
    if (src_count == 1) {
        dst->array = NULL;
        dst->count = 0;
        return 0;
    }

    if (ecs_id_is_wildcard(without)) {
        if (ECS_IS_PAIR(without)) {
            ecs_entity_t r = ECS_PAIR_FIRST(without);
            ecs_entity_t o = ECS_PAIR_SECOND(without);
            if (r == EcsWildcard && o != EcsWildcard) {
                return flecs_type_new_filtered(world, dst, src, without, at);
            }
        }
        count += flecs_type_count_matches(src, without, at + 1);
    }

    int32_t dst_count = src_count - count;
    dst->count = dst_count;
    if (!dst_count) {
        dst->array = NULL;
        return 0;
    }

    ecs_id_t *dst_array = flecs_walloc_n(world, ecs_id_t, dst_count);
    dst->array = dst_array;

    if (at) {
        ecs_os_memcpy_n(dst_array, src_array, ecs_id_t, at);
    }

    int32_t remain = dst_count - at;
    if (remain) {
        ecs_os_memcpy_n(
            &dst_array[at], &src_array[at + count], ecs_id_t, remain);
    }

    return 0;
}

/* Copy type */
ecs_type_t flecs_type_copy(
    ecs_world_t *world,
    const ecs_type_t *src)
{
    int32_t src_count = src->count;
    if (!src_count) {
        return (ecs_type_t){ 0 };
    }

    ecs_id_t *ids = flecs_walloc_n(world, ecs_id_t, src_count);
    ecs_os_memcpy_n(ids, src->array, ecs_id_t, src_count);
    return (ecs_type_t) {
        .array = ids,
        .count = src_count
    };
}

/* Free type */
void flecs_type_free(
    ecs_world_t *world,
    ecs_type_t *type)
{
    int32_t count = type->count;
    if (count) {
        flecs_wfree_n(world, ecs_id_t, type->count, type->array);
    }
}

/* Add to type */
static
void flecs_type_add(
    ecs_world_t *world,
    ecs_type_t *type,
    ecs_id_t add)
{
    ecs_type_t new_type;
    int res = flecs_type_new_with(world, &new_type, type, add);
    if (res != -1) {
        flecs_type_free(world, type);
        type->array = new_type.array;
        type->count = new_type.count;
    }
}

/* Graph edge utilities */

void flecs_table_diff_builder_init(
    ecs_world_t *world,
    ecs_table_diff_builder_t *builder)
{
    ecs_allocator_t *a = &world->allocator;
    ecs_vec_init_t(a, &builder->added, ecs_id_t, 256);
    ecs_vec_init_t(a, &builder->removed, ecs_id_t, 256);
}

void flecs_table_diff_builder_fini(
    ecs_world_t *world,
    ecs_table_diff_builder_t *builder)
{
    ecs_allocator_t *a = &world->allocator;
    ecs_vec_fini_t(a, &builder->added, ecs_id_t);
    ecs_vec_fini_t(a, &builder->removed, ecs_id_t);
}

void flecs_table_diff_builder_clear(
    ecs_table_diff_builder_t *builder)
{
    ecs_vec_clear(&builder->added);
    ecs_vec_clear(&builder->removed);
}

static
void flecs_table_diff_build_type(
    ecs_world_t *world,
    ecs_vec_t *vec,
    ecs_type_t *type,
    int32_t offset)
{
    int32_t count = vec->count - offset;
    ecs_assert(count >= 0, ECS_INTERNAL_ERROR, NULL);
    if (count) {
        type->array = flecs_wdup_n(world, ecs_id_t, count, 
            ECS_ELEM_T(vec->array, ecs_id_t, offset));
        type->count = count;
        ecs_vec_set_count_t(&world->allocator, vec, ecs_id_t, offset);
    }
}

void flecs_table_diff_build(
    ecs_world_t *world,
    ecs_table_diff_builder_t *builder,
    ecs_table_diff_t *diff,
    int32_t added_offset,
    int32_t removed_offset)
{
    flecs_table_diff_build_type(world, &builder->added, &diff->added, 
        added_offset);
    flecs_table_diff_build_type(world, &builder->removed, &diff->removed, 
        removed_offset);
}

void flecs_table_diff_build_noalloc(
    ecs_table_diff_builder_t *builder,
    ecs_table_diff_t *diff)
{
    diff->added = (ecs_type_t){
        .array = builder->added.array, .count = builder->added.count };
    diff->removed = (ecs_type_t){
        .array = builder->removed.array, .count = builder->removed.count };
}

static
void flecs_table_diff_build_add_type_to_vec(
    ecs_world_t *world,
    ecs_vec_t *vec,
    ecs_type_t *add)
{
    if (!add || !add->count) {
        return;
    }

    int32_t offset = vec->count;
    ecs_vec_grow_t(&world->allocator, vec, ecs_id_t, add->count);
    ecs_os_memcpy_n(ecs_vec_get_t(vec, ecs_id_t, offset),
        add->array, ecs_id_t, add->count);
}

void flecs_table_diff_build_append_table(
    ecs_world_t *world,
    ecs_table_diff_builder_t *dst,
    ecs_table_diff_t *src)
{
    flecs_table_diff_build_add_type_to_vec(world, &dst->added, &src->added);
    flecs_table_diff_build_add_type_to_vec(world, &dst->removed, &src->removed);
}

static
void flecs_table_diff_free(
    ecs_world_t *world,
    ecs_table_diff_t *diff) 
{
    flecs_wfree_n(world, ecs_id_t, diff->added.count, diff->added.array);
    flecs_wfree_n(world, ecs_id_t, diff->removed.count, diff->removed.array);
    flecs_bfree(&world->allocators.table_diff, diff);
}

static
ecs_graph_edge_t* flecs_table_ensure_hi_edge(
    ecs_world_t *world,
    ecs_graph_edges_t *edges,
    ecs_id_t id)
{
    if (!edges->hi) {
        edges->hi = flecs_alloc_t(&world->allocator, ecs_map_t);
        ecs_map_init_w_params(edges->hi, &world->allocators.ptr);
    }

    ecs_graph_edge_t **r = ecs_map_ensure_ref(edges->hi, ecs_graph_edge_t, id);
    ecs_graph_edge_t *edge = r[0];
    if (edge) {
        return edge;
    }

    if (id < FLECS_HI_COMPONENT_ID) {
        edge = &edges->lo[id];
    } else {
        edge = flecs_bcalloc(&world->allocators.graph_edge);
    }

    r[0] = edge;
    return edge;
}

static
ecs_graph_edge_t* flecs_table_ensure_edge(
    ecs_world_t *world,
    ecs_graph_edges_t *edges,
    ecs_id_t id)
{
    ecs_graph_edge_t *edge;
    
    if (id < FLECS_HI_COMPONENT_ID) {
        if (!edges->lo) {
            edges->lo = flecs_bcalloc(&world->allocators.graph_edge_lo);
        }
        edge = &edges->lo[id];
    } else {
        edge = flecs_table_ensure_hi_edge(world, edges, id);
    }

    return edge;
}

static
void flecs_table_disconnect_edge(
    ecs_world_t *world,
    ecs_id_t id,
    ecs_graph_edge_t *edge)
{
    ecs_assert(edge != NULL, ECS_INTERNAL_ERROR, NULL);
    ecs_assert(edge->id == id, ECS_INTERNAL_ERROR, NULL);
    (void)id;

    /* Remove backref from destination table */
    ecs_graph_edge_hdr_t *next = edge->hdr.next;
    ecs_graph_edge_hdr_t *prev = edge->hdr.prev;

    if (next) {
        next->prev = prev;
    }
    if (prev) {
        prev->next = next;
    }

    /* Remove data associated with edge */
    ecs_table_diff_t *diff = edge->diff;
    if (diff) {
        flecs_table_diff_free(world, diff);
    }

    /* If edge id is low, clear it from fast lookup array */
    if (id < FLECS_HI_COMPONENT_ID) {
        ecs_os_memset_t(edge, 0, ecs_graph_edge_t);
    } else {
        flecs_bfree(&world->allocators.graph_edge, edge);
    }
}

static
void flecs_table_remove_edge(
    ecs_world_t *world,
    ecs_graph_edges_t *edges,
    ecs_id_t id,
    ecs_graph_edge_t *edge)
{
    ecs_assert(edges != NULL, ECS_INTERNAL_ERROR, NULL);
    ecs_assert(edges->hi != NULL, ECS_INTERNAL_ERROR, NULL);
    flecs_table_disconnect_edge(world, id, edge);
    ecs_map_remove(edges->hi, id);
}

static
void flecs_table_init_edges(
    ecs_graph_edges_t *edges)
{
    edges->lo = NULL;
    edges->hi = NULL;
}

static
void flecs_table_init_node(
    ecs_graph_node_t *node)
{
    flecs_table_init_edges(&node->add);
    flecs_table_init_edges(&node->remove);
}

bool flecs_table_records_update_empty(
    ecs_table_t *table)
{
    bool result = false;
    bool is_empty = ecs_table_count(table) == 0;

    int32_t i, count = table->_->record_count;
    for (i = 0; i < count; i ++) {
        ecs_table_record_t *tr = &table->_->records[i];
        ecs_table_cache_t *cache = tr->hdr.cache;
        result |= ecs_table_cache_set_empty(cache, table, is_empty);
    }

    return result;
}

static
void flecs_init_table(
    ecs_world_t *world,
    ecs_table_t *table,
    ecs_table_t *prev)
{
    table->type_info = NULL;
    table->flags = 0;
    table->dirty_state = NULL;
    table->_->lock = 0;
    table->_->refcount = 1;
    table->_->generation = 0;

    flecs_table_init_node(&table->node);

    flecs_table_init(world, table, prev);
}

static
ecs_table_t *flecs_create_table(
    ecs_world_t *world,
    ecs_type_t *type,
    flecs_hashmap_result_t table_elem,
    ecs_table_t *prev)
{
    ecs_table_t *result = flecs_sparse_add_t(&world->store.tables, ecs_table_t);
    ecs_assert(result != NULL, ECS_INTERNAL_ERROR, NULL);
    result->_ = flecs_calloc_t(&world->allocator, ecs_table__t);
    ecs_assert(result->_ != NULL, ECS_INTERNAL_ERROR, NULL);
    
    result->id = flecs_sparse_last_id(&world->store.tables);
    result->type = *type;

    if (ecs_should_log_2()) {
        char *expr = ecs_type_str(world, &result->type);
        ecs_dbg_2(
            "#[green]table#[normal] [%s] #[green]created#[reset] with id %d", 
            expr, result->id);
        ecs_os_free(expr);
    }

    ecs_log_push_2();

    /* Store table in table hashmap */
    *(ecs_table_t**)table_elem.value = result;

    /* Set keyvalue to one that has the same lifecycle as the table */
    *(ecs_type_t*)table_elem.key = result->type;
    result->_->hash = table_elem.hash;

    flecs_init_table(world, result, prev);

    /* Update counters */
    world->info.table_count ++;
    world->info.table_record_count += result->_->record_count;
    world->info.table_storage_count += result->storage_count;
    world->info.empty_table_count ++;
    world->info.table_create_total ++;
    
    if (!result->storage_count) {
        world->info.tag_table_count ++;
    } else {
        world->info.trivial_table_count += !(result->flags & EcsTableIsComplex);
    }

    ecs_log_pop_2();

    return result;
}

static
ecs_table_t* flecs_table_ensure(
    ecs_world_t *world,
    ecs_type_t *type,
    bool own_type,
    ecs_table_t *prev)
{    
    ecs_poly_assert(world, ecs_world_t);   

    int32_t id_count = type->count;
    if (!id_count) {
        return &world->store.root;
    }

    ecs_table_t *table;
    flecs_hashmap_result_t elem = flecs_hashmap_ensure(
        &world->store.table_map, type, ecs_table_t*);
    if ((table = *(ecs_table_t**)elem.value)) {
        if (own_type) {
            flecs_type_free(world, type);
        }
        return table;
    }

    /* If we get here, table needs to be created which is only allowed when the
     * application is not currently in progress */
    ecs_assert(!(world->flags & EcsWorldReadonly), ECS_INTERNAL_ERROR, NULL);

    /* If we get here, the table has not been found, so create it. */
    if (own_type) {
        return flecs_create_table(world, type, elem, prev);
    }

    ecs_type_t copy = flecs_type_copy(world, type);
    return flecs_create_table(world, &copy, elem, prev);
}

static
void flecs_diff_insert_added(
    ecs_world_t *world,
    ecs_table_diff_builder_t *diff,
    ecs_id_t id)
{
    ecs_vec_append_t(&world->allocator, &diff->added, ecs_id_t)[0] = id;
}

static
void flecs_diff_insert_removed(
    ecs_world_t *world,
    ecs_table_diff_builder_t *diff,
    ecs_id_t id)
{
    ecs_allocator_t *a = &world->allocator;
    ecs_vec_append_t(a, &diff->removed, ecs_id_t)[0] = id;
}

static
void flecs_compute_table_diff(
    ecs_world_t *world,
    ecs_table_t *node,
    ecs_table_t *next,
    ecs_graph_edge_t *edge,
    ecs_id_t id)
{
    if (ECS_IS_PAIR(id)) {
        ecs_id_record_t *idr = flecs_id_record_get(world, ecs_pair(
            ECS_PAIR_FIRST(id), EcsWildcard));
        if (idr->flags & EcsIdUnion) {
            if (node != next) {
                id = ecs_pair(EcsUnion, ECS_PAIR_FIRST(id));
            } else {
                ecs_table_diff_t *diff = flecs_bcalloc(
                    &world->allocators.table_diff);
                diff->added.count = 1;
                diff->added.array = flecs_wdup_n(world, ecs_id_t, 1, &id);
                edge->diff = diff;
                return;
            }
        }
    }

    ecs_type_t node_type = node->type;
    ecs_type_t next_type = next->type;

    ecs_id_t *ids_node = node_type.array;
    ecs_id_t *ids_next = next_type.array;
    int32_t i_node = 0, node_count = node_type.count;
    int32_t i_next = 0, next_count = next_type.count;
    int32_t added_count = 0;
    int32_t removed_count = 0;
    bool trivial_edge = !ECS_HAS_RELATION(id, EcsIsA);

    /* First do a scan to see how big the diff is, so we don't have to realloc
     * or alloc more memory than required. */
    for (; i_node < node_count && i_next < next_count; ) {
        ecs_id_t id_node = ids_node[i_node];
        ecs_id_t id_next = ids_next[i_next];

        bool added = id_next < id_node;
        bool removed = id_node < id_next;

        trivial_edge &= !added || id_next == id;
        trivial_edge &= !removed || id_node == id;

        added_count += added;
        removed_count += removed;

        i_node += id_node <= id_next;
        i_next += id_next <= id_node;
    }

    added_count += next_count - i_next;
    removed_count += node_count - i_node;

    trivial_edge &= (added_count + removed_count) <= 1 && 
        !ecs_id_is_wildcard(id);

    if (trivial_edge) {
        /* If edge is trivial there's no need to create a diff element for it */
        return;
    }

    ecs_table_diff_builder_t *builder = &world->allocators.diff_builder;
    int32_t added_offset = builder->added.count;
    int32_t removed_offset = builder->removed.count;

    for (i_node = 0, i_next = 0; i_node < node_count && i_next < next_count; ) {
        ecs_id_t id_node = ids_node[i_node];
        ecs_id_t id_next = ids_next[i_next];

        if (id_next < id_node) {
            flecs_diff_insert_added(world, builder, id_next);
        } else if (id_node < id_next) {
            flecs_diff_insert_removed(world, builder, id_node);
        }

        i_node += id_node <= id_next;
        i_next += id_next <= id_node;
    }

    for (; i_next < next_count; i_next ++) {
        flecs_diff_insert_added(world, builder, ids_next[i_next]);
    }
    for (; i_node < node_count; i_node ++) {
        flecs_diff_insert_removed(world, builder, ids_node[i_node]);
    }

    ecs_table_diff_t *diff = flecs_bcalloc(&world->allocators.table_diff);
    edge->diff = diff;
    flecs_table_diff_build(world, builder, diff, added_offset, removed_offset);

    ecs_assert(diff->added.count == added_count, ECS_INTERNAL_ERROR, NULL);
    ecs_assert(diff->removed.count == removed_count, ECS_INTERNAL_ERROR, NULL);
}

static
void flecs_add_overrides_for_base(
    ecs_world_t *world,
    ecs_type_t *dst_type,
    ecs_id_t pair)
{
    ecs_entity_t base = ecs_pair_second(world, pair);
    ecs_assert(base != 0, ECS_INTERNAL_ERROR, NULL);
    ecs_table_t *base_table = ecs_get_table(world, base);
    if (!base_table) {
        return;
    }

    ecs_id_t *ids = base_table->type.array;

    ecs_flags32_t flags = base_table->flags;
    if (flags & EcsTableHasOverrides) {
        int32_t i, count = base_table->type.count;
        for (i = 0; i < count; i ++) {
            ecs_id_t id = ids[i];
            if (ECS_HAS_ID_FLAG(id, OVERRIDE)) {
                flecs_type_add(world, dst_type, id & ~ECS_OVERRIDE);
            } else {
                ecs_table_record_t *tr = &base_table->_->records[i];
                ecs_id_record_t *idr = (ecs_id_record_t*)tr->hdr.cache;
                if (idr->flags & EcsIdAlwaysOverride) {
                    flecs_type_add(world, dst_type, id);
                }
            }
        }
    }

    if (flags & EcsTableHasIsA) {
        const ecs_table_record_t *tr = flecs_id_record_get_table(
            world->idr_isa_wildcard, base_table);
        ecs_assert(tr != NULL, ECS_INTERNAL_ERROR, NULL);
        int32_t i = tr->column, end = i + tr->count;
        for (; i != end; i ++) {
            flecs_add_overrides_for_base(world, dst_type, ids[i]);
        }
    }
}

static
void flecs_add_with_property(
    ecs_world_t *world,
    ecs_id_record_t *idr_with_wildcard,
    ecs_type_t *dst_type,
    ecs_entity_t r,
    ecs_entity_t o)
{
    r = ecs_get_alive(world, r);

    /* Check if component/relationship has With pairs, which contain ids
     * that need to be added to the table. */
    ecs_table_t *table = ecs_get_table(world, r);
    if (!table) {
        return;
    }
    
    const ecs_table_record_t *tr = flecs_id_record_get_table(
        idr_with_wildcard, table);
    if (tr) {
        int32_t i = tr->column, end = i + tr->count;
        ecs_id_t *ids = table->type.array;

        for (; i < end; i ++) {
            ecs_id_t id = ids[i];
            ecs_assert(ECS_PAIR_FIRST(id) == EcsWith, ECS_INTERNAL_ERROR, NULL);
            ecs_id_t ra = ECS_PAIR_SECOND(id);
            ecs_id_t a = ra;
            if (o) {
                a = ecs_pair(ra, o);
            }

            flecs_type_add(world, dst_type, a);
            flecs_add_with_property(world, idr_with_wildcard, dst_type, ra, o);
        }
    }

}

static
ecs_table_t* flecs_find_table_with(
    ecs_world_t *world,
    ecs_table_t *node,
    ecs_id_t with)
{    
    ecs_ensure_id(world, with);

    ecs_id_record_t *idr = NULL;
    ecs_entity_t r = 0, o = 0;

    if (ECS_IS_PAIR(with)) {
        r = ECS_PAIR_FIRST(with);
        o = ECS_PAIR_SECOND(with);
        idr = flecs_id_record_ensure(world, ecs_pair(r, EcsWildcard));
        if (idr->flags & EcsIdUnion) {
            ecs_type_t dst_type;
            ecs_id_t union_id = ecs_pair(EcsUnion, r);
            int res = flecs_type_new_with(
                world, &dst_type, &node->type, union_id);
            if (res == -1) {
                return node;
            }

            return flecs_table_ensure(world, &dst_type, true, node);
        } else if (idr->flags & EcsIdExclusive) {
            /* Relationship is exclusive, check if table already has it */
            const ecs_table_record_t *tr = flecs_id_record_get_table(idr, node);
            if (tr) {
                /* Table already has an instance of the relationship, create
                 * a new id sequence with the existing id replaced */
                ecs_type_t dst_type = flecs_type_copy(world, &node->type);
                ecs_assert(dst_type.array != NULL, ECS_INTERNAL_ERROR, NULL);
                dst_type.array[tr->column] = with;
                return flecs_table_ensure(world, &dst_type, true, node);
            }
        }
    } else {
        idr = flecs_id_record_ensure(world, with);
        r = with;
    }

    /* Create sequence with new id */
    ecs_type_t dst_type;
    int res = flecs_type_new_with(world, &dst_type, &node->type, with);
    if (res == -1) {
        return node; /* Current table already has id */
    }

    if (r == EcsIsA) {
        /* If adding a prefab, check if prefab has overrides */
        flecs_add_overrides_for_base(world, &dst_type, with);
    } else if (r == EcsChildOf) {
        o = ecs_get_alive(world, o);
        if (ecs_has_id(world, o, EcsPrefab)) {
            flecs_type_add(world, &dst_type, EcsPrefab);
        }
    }

    if (idr->flags & EcsIdWith) {
        ecs_id_record_t *idr_with_wildcard = flecs_id_record_get(world,
            ecs_pair(EcsWith, EcsWildcard));
        /* If id has With property, add targets to type */
        flecs_add_with_property(world, idr_with_wildcard, &dst_type, r, o);
    }

    return flecs_table_ensure(world, &dst_type, true, node);
}

static
ecs_table_t* flecs_find_table_without(
    ecs_world_t *world,
    ecs_table_t *node,
    ecs_id_t without)
{
    if (ECS_IS_PAIR(without)) {
        ecs_entity_t r = 0;
        ecs_id_record_t *idr = NULL;
        r = ECS_PAIR_FIRST(without);
        idr = flecs_id_record_get(world, ecs_pair(r, EcsWildcard));
        if (idr && idr->flags & EcsIdUnion) {
            without = ecs_pair(EcsUnion, r);
        }
    }

    /* Create sequence with new id */
    ecs_type_t dst_type;
    int res = flecs_type_new_without(world, &dst_type, &node->type, without);
    if (res == -1) {
        return node; /* Current table does not have id */
    }

    return flecs_table_ensure(world, &dst_type, true, node);
}

static
void flecs_table_init_edge(
    ecs_table_t *table,
    ecs_graph_edge_t *edge,
    ecs_id_t id,
    ecs_table_t *to)
{
    ecs_assert(table != NULL, ECS_INTERNAL_ERROR, NULL);
    ecs_assert(edge != NULL, ECS_INTERNAL_ERROR, NULL);
    ecs_assert(edge->id == 0, ECS_INTERNAL_ERROR, NULL);
    ecs_assert(edge->hdr.next == NULL, ECS_INTERNAL_ERROR, NULL);
    ecs_assert(edge->hdr.prev == NULL, ECS_INTERNAL_ERROR, NULL);
    
    edge->from = table;
    edge->to = to;
    edge->id = id;
}

static
void flecs_init_edge_for_add(
    ecs_world_t *world,
    ecs_table_t *table,
    ecs_graph_edge_t *edge,
    ecs_id_t id,
    ecs_table_t *to)
{
    flecs_table_init_edge(table, edge, id, to);

    flecs_table_ensure_hi_edge(world, &table->node.add, id);

    if (table != to || table->flags & EcsTableHasUnion) {
        /* Add edges are appended to refs.next */
        ecs_graph_edge_hdr_t *to_refs = &to->node.refs;
        ecs_graph_edge_hdr_t *next = to_refs->next;
        
        to_refs->next = &edge->hdr;
        edge->hdr.prev = to_refs;

        edge->hdr.next = next;
        if (next) {
            next->prev = &edge->hdr;
        }

        flecs_compute_table_diff(world, table, to, edge, id);
    }
}

static
void flecs_init_edge_for_remove(
    ecs_world_t *world,
    ecs_table_t *table,
    ecs_graph_edge_t *edge,
    ecs_id_t id,
    ecs_table_t *to)
{
    flecs_table_init_edge(table, edge, id, to);

    flecs_table_ensure_hi_edge(world, &table->node.remove, id);

    if (table != to) {
        /* Remove edges are appended to refs.prev */
        ecs_graph_edge_hdr_t *to_refs = &to->node.refs;
        ecs_graph_edge_hdr_t *prev = to_refs->prev;

        to_refs->prev = &edge->hdr;
        edge->hdr.next = to_refs;

        edge->hdr.prev = prev;
        if (prev) {
            prev->next = &edge->hdr;
        }

        flecs_compute_table_diff(world, table, to, edge, id);
    }
}

static
ecs_table_t* flecs_create_edge_for_remove(
    ecs_world_t *world,
    ecs_table_t *node,
    ecs_graph_edge_t *edge,
    ecs_id_t id)
{
    ecs_table_t *to = flecs_find_table_without(world, node, id);
    flecs_init_edge_for_remove(world, node, edge, id, to);
    return to;   
}

static
ecs_table_t* flecs_create_edge_for_add(
    ecs_world_t *world,
    ecs_table_t *node,
    ecs_graph_edge_t *edge,
    ecs_id_t id)
{
    ecs_table_t *to = flecs_find_table_with(world, node, id);
    flecs_init_edge_for_add(world, node, edge, id, to);
    return to;
}

ecs_table_t* flecs_table_traverse_remove(
    ecs_world_t *world,
    ecs_table_t *node,
    ecs_id_t *id_ptr,
    ecs_table_diff_t *diff)
{
    ecs_poly_assert(world, ecs_world_t);

    node = node ? node : &world->store.root;

    /* Removing 0 from an entity is not valid */
    ecs_check(id_ptr != NULL, ECS_INVALID_PARAMETER, NULL);
    ecs_check(id_ptr[0] != 0, ECS_INVALID_PARAMETER, NULL);

    ecs_id_t id = id_ptr[0];
    ecs_graph_edge_t *edge = flecs_table_ensure_edge(world, &node->node.remove, id);
    ecs_table_t *to = edge->to;

    if (!to) {
        to = flecs_create_edge_for_remove(world, node, edge, id);
        ecs_assert(to != NULL, ECS_INTERNAL_ERROR, NULL);
        ecs_assert(edge->to != NULL, ECS_INTERNAL_ERROR, NULL);
    }

    if (node != to) {
        if (edge->diff) {
            *diff = *edge->diff;
        } else {
            diff->added.count = 0;
            diff->removed.array = id_ptr;
            diff->removed.count = 1;
        }
    }

    return to;
error:
    return NULL;
}

ecs_table_t* flecs_table_traverse_add(
    ecs_world_t *world,
    ecs_table_t *node,
    ecs_id_t *id_ptr,
    ecs_table_diff_t *diff)
{
    ecs_poly_assert(world, ecs_world_t);
    ecs_assert(diff != NULL, ECS_INTERNAL_ERROR, NULL);

    node = node ? node : &world->store.root;

    /* Adding 0 to an entity is not valid */
    ecs_check(id_ptr != NULL, ECS_INVALID_PARAMETER, NULL);
    ecs_check(id_ptr[0] != 0, ECS_INVALID_PARAMETER, NULL);

    ecs_id_t id = id_ptr[0];
    ecs_graph_edge_t *edge = flecs_table_ensure_edge(world, &node->node.add, id);
    ecs_table_t *to = edge->to;

    if (!to) {
        to = flecs_create_edge_for_add(world, node, edge, id);
        ecs_assert(to != NULL, ECS_INTERNAL_ERROR, NULL);
        ecs_assert(edge->to != NULL, ECS_INTERNAL_ERROR, NULL);
    }

    if (node != to || edge->diff) {
        if (edge->diff) {
            *diff = *edge->diff;
        } else {
            diff->added.array = id_ptr;
            diff->added.count = 1;
            diff->removed.count = 0;
        }
    }

    return to;
error:
    return NULL;
}

ecs_table_t* flecs_table_find_or_create(
    ecs_world_t *world,
    ecs_type_t *type)
{
    ecs_poly_assert(world, ecs_world_t);
    return flecs_table_ensure(world, type, false, NULL);
}

void flecs_init_root_table(
    ecs_world_t *world)
{
    ecs_poly_assert(world, ecs_world_t);

    world->store.root.type = (ecs_type_t){0};
    world->store.root._ = flecs_calloc_t(&world->allocator, ecs_table__t);
    flecs_init_table(world, &world->store.root, NULL);

    /* Ensure table indices start at 1, as 0 is reserved for the root */
    uint64_t new_id = flecs_sparse_new_id(&world->store.tables);
    ecs_assert(new_id == 0, ECS_INTERNAL_ERROR, NULL);
    (void)new_id;
}

void flecs_table_clear_edges(
    ecs_world_t *world,
    ecs_table_t *table)
{
    (void)world;
    ecs_poly_assert(world, ecs_world_t);

    ecs_log_push_1();

    ecs_map_iter_t it;
    ecs_graph_node_t *table_node = &table->node;
    ecs_graph_edges_t *node_add = &table_node->add;
    ecs_graph_edges_t *node_remove = &table_node->remove;
    ecs_map_t *add_hi = node_add->hi;
    ecs_map_t *remove_hi = node_remove->hi;
    ecs_graph_edge_hdr_t *node_refs = &table_node->refs;

    /* Cleanup outgoing edges */
    it = ecs_map_iter(add_hi);
    while (ecs_map_next(&it)) {
        flecs_table_disconnect_edge(world, ecs_map_key(&it), ecs_map_ptr(&it));
    }

    it = ecs_map_iter(remove_hi);
    while (ecs_map_next(&it)) {
        flecs_table_disconnect_edge(world, ecs_map_key(&it), ecs_map_ptr(&it));
    }

    /* Cleanup incoming add edges */
    ecs_graph_edge_hdr_t *next, *cur = node_refs->next;
    if (cur) {
        do {
            ecs_graph_edge_t *edge = (ecs_graph_edge_t*)cur;
            ecs_assert(edge->to == table, ECS_INTERNAL_ERROR, NULL);
            ecs_assert(edge->from != NULL, ECS_INTERNAL_ERROR, NULL);
            next = cur->next;
            flecs_table_remove_edge(world, &edge->from->node.add, edge->id, edge);
        } while ((cur = next));
    }

    /* Cleanup incoming remove edges */
    cur = node_refs->prev;
    if (cur) {
        do {
            ecs_graph_edge_t *edge = (ecs_graph_edge_t*)cur;
            ecs_assert(edge->to == table, ECS_INTERNAL_ERROR, NULL);
            ecs_assert(edge->from != NULL, ECS_INTERNAL_ERROR, NULL);
            next = cur->prev;
            flecs_table_remove_edge(world, &edge->from->node.remove, edge->id, edge);
        } while ((cur = next));
    }

    if (node_add->lo) {
        flecs_bfree(&world->allocators.graph_edge_lo, node_add->lo);
    }
    if (node_remove->lo) {
        flecs_bfree(&world->allocators.graph_edge_lo, node_remove->lo);
    }

    ecs_map_fini(add_hi);
    ecs_map_fini(remove_hi);
    flecs_free_t(&world->allocator, ecs_map_t, add_hi);
    flecs_free_t(&world->allocator, ecs_map_t, remove_hi);
    table_node->add.lo = NULL;
    table_node->remove.lo = NULL;
    table_node->add.hi = NULL;
    table_node->remove.hi = NULL;

    ecs_log_pop_1();
}

/* Public convenience functions for traversing table graph */
ecs_table_t* ecs_table_add_id(
    ecs_world_t *world,
    ecs_table_t *table,
    ecs_id_t id)
{
    ecs_table_diff_t diff;
    return flecs_table_traverse_add(world, table, &id, &diff);
}

ecs_table_t* ecs_table_remove_id(
    ecs_world_t *world,
    ecs_table_t *table,
    ecs_id_t id)
{
    ecs_table_diff_t diff;
    return flecs_table_traverse_remove(world, table, &id, &diff);
}

ecs_table_t* ecs_table_find(
    ecs_world_t *world,
    const ecs_id_t *ids,
    int32_t id_count)
{
    ecs_type_t type = {
        .array = (ecs_id_t*)ids,
        .count = id_count
    };
    return flecs_table_ensure(world, &type, false, NULL);
}

/**
 * @file iter.c
 * @brief Iterator API.
 * 
 * The iterator API contains functions that apply to all iterators, such as
 * resource management, or fetching resources for a matched table. The API also
 * contains functions for generic iterators, which make it possible to iterate
 * an iterator without needing to know what created the iterator.
 */

#include <stddef.h>

/* Utility macros to enforce consistency when initializing iterator fields */

/* If term count is smaller than cache size, initialize with inline array,
 * otherwise allocate. */
#define INIT_CACHE(it, stack, fields, f, T, count)\
    if (!it->f && (fields & flecs_iter_cache_##f) && count) {\
        it->f = flecs_stack_calloc_n(stack, T, count);\
        it->priv.cache.used |= flecs_iter_cache_##f;\
    }

/* If array is allocated, free it when finalizing the iterator */
#define FINI_CACHE(it, f, T, count)\
    if (it->priv.cache.used & flecs_iter_cache_##f) {\
        flecs_stack_free_n((void*)it->f, T, count);\
    }

void* flecs_iter_calloc(
    ecs_iter_t *it,
    ecs_size_t size,
    ecs_size_t align)
{
    ecs_world_t *world = it->world;
    ecs_stage_t *stage = flecs_stage_from_world((ecs_world_t**)&world);
    ecs_stack_t *stack = &stage->allocators.iter_stack;
    return flecs_stack_calloc(stack, size, align); 
}

void flecs_iter_free(
    void *ptr,
    ecs_size_t size)
{
    flecs_stack_free(ptr, size);
}

void flecs_iter_init(
    const ecs_world_t *world,
    ecs_iter_t *it,
    ecs_flags8_t fields)
{
    ecs_assert(!ECS_BIT_IS_SET(it->flags, EcsIterIsValid), 
        ECS_INTERNAL_ERROR, NULL);

    ecs_stage_t *stage = flecs_stage_from_world((ecs_world_t**)&world);
    ecs_stack_t *stack = &stage->allocators.iter_stack;

    it->priv.cache.used = 0;
    it->priv.cache.allocated = 0;
    it->priv.cache.stack_cursor = flecs_stack_get_cursor(stack);
    it->priv.entity_iter = flecs_stack_calloc_t(
        stack, ecs_entity_filter_iter_t);

    INIT_CACHE(it, stack, fields, ids, ecs_id_t, it->field_count);
    INIT_CACHE(it, stack, fields, sources, ecs_entity_t, it->field_count);
    INIT_CACHE(it, stack, fields, match_indices, int32_t, it->field_count);
    INIT_CACHE(it, stack, fields, columns, int32_t, it->field_count);
    INIT_CACHE(it, stack, fields, variables, ecs_var_t, it->variable_count);
    INIT_CACHE(it, stack, fields, ptrs, void*, it->field_count);
}

void flecs_iter_validate(
    ecs_iter_t *it)
{
    ECS_BIT_SET(it->flags, EcsIterIsValid);

    /* Make sure multithreaded iterator isn't created for real world */
    ecs_world_t *world = it->real_world;
    ecs_poly_assert(world, ecs_world_t);
    ecs_check(!(world->flags & EcsWorldMultiThreaded) || it->world != it->real_world,
        ECS_INVALID_PARAMETER, 
            "create iterator for stage when world is in multithreaded mode");
    (void)world;
error:
    return;
}

void ecs_iter_fini(
    ecs_iter_t *it)
{
    ECS_BIT_CLEAR(it->flags, EcsIterIsValid);

    if (it->fini) {
        it->fini(it);
    }

    ecs_world_t *world = it->world;
    if (!world) {
        return;
    }

    FINI_CACHE(it, ids, ecs_id_t, it->field_count);
    FINI_CACHE(it, sources, ecs_entity_t, it->field_count);
    FINI_CACHE(it, match_indices, int32_t, it->field_count);
    FINI_CACHE(it, columns, int32_t, it->field_count);
    FINI_CACHE(it, variables, ecs_var_t, it->variable_count);
    FINI_CACHE(it, ptrs, void*, it->field_count);
    flecs_stack_free_t(it->priv.entity_iter, ecs_entity_filter_iter_t);

    ecs_stage_t *stage = flecs_stage_from_world(&world);
    flecs_stack_restore_cursor(&stage->allocators.iter_stack, 
        &it->priv.cache.stack_cursor);
}

static
bool flecs_iter_populate_term_data(
    ecs_world_t *world,
    ecs_iter_t *it,
    int32_t t,
    int32_t column,
    void **ptr_out)
{
    bool is_shared = false;
    ecs_table_t *table;
    void *data;
    int32_t row, u_index;

    if (!column) {
        /* Term has no data. This includes terms that have Not operators. */
        goto no_data;
    }

    /* Filter terms may match with data but don't return it */
    if (it->terms[t].inout == EcsInOutNone) {
        goto no_data;
    }

    ecs_assert(it->sizes != NULL, ECS_INTERNAL_ERROR, NULL);
    int32_t size = it->sizes[t];
    if (!size) {
        goto no_data;
    }

    if (column < 0) {
        table = it->table;
        is_shared = true;

        /* Data is not from This */
        if (it->references && (!table || !(table->flags & EcsTableHasTarget))) {
            /* The reference array is used only for components matched on a
             * table (vs. individual entities). Remaining components should be
             * assigned outside of this function */
            if (ecs_term_match_this(&it->terms[t])) {

                /* Iterator provides cached references for non-This terms */
                ecs_ref_t *ref = &it->references[-column - 1];
                if (ptr_out) {
                    if (ref->id) {
                        ptr_out[0] = (void*)ecs_ref_get_id(world, ref, ref->id);
                    } else {
                        ptr_out[0] = NULL;
                    }
                }

                if (!ref->id) {
                    is_shared = false;
                }

                return is_shared;
            }

            return true;
        } else {
            ecs_entity_t subj = it->sources[t];
            ecs_assert(subj != 0, ECS_INTERNAL_ERROR, NULL);

            /* Don't use ecs_get_id directly. Instead, go directly to the
             * storage so that we can get both the pointer and size */
            ecs_record_t *r = flecs_entities_get(world, subj);
            ecs_assert(r != NULL && r->table != NULL, ECS_INTERNAL_ERROR, NULL);

            row = ECS_RECORD_TO_ROW(r->row);
            table = r->table;

            ecs_id_t id = it->ids[t];
            ecs_table_t *s_table = table->storage_table;
            ecs_table_record_t *tr;

            if (!s_table || !(tr = flecs_table_record_get(world, s_table, id))){
                u_index = flecs_table_column_to_union_index(table, -column - 1);
                if (u_index != -1) {
                    goto has_union;
                }
                goto no_data;
            }

            /* We now have row and column, so we can get the storage for the id
             * which gives us the pointer and size */
            column = tr->column;
            ecs_vec_t *s = &table->data.columns[column];
            data = ecs_vec_first(s);
            /* Fallthrough to has_data */
        }
    } else {
        /* Data is from This, use table from iterator */
        table = it->table;
        ecs_assert(table != NULL, ECS_INTERNAL_ERROR, NULL);

        row = it->offset;

        int32_t storage_column = ecs_table_type_to_storage_index(
            table, column - 1);
        if (storage_column == -1) {
            u_index = flecs_table_column_to_union_index(table, column - 1);
            if (u_index != -1) {
                goto has_union;
            }
            goto no_data;
        }

        if (!it->count) {
            goto no_data;
        }

        ecs_vec_t *s = &table->data.columns[storage_column];
        data = ecs_vec_first(s);

        /* Fallthrough to has_data */
    }

has_data:
    if (ptr_out) ptr_out[0] = ECS_ELEM(data, size, row);
    return is_shared;

has_union: {
        /* Edge case: if column is a switch we should return the vector with case
         * identifiers. Will be replaced in the future with pluggable storage */
        ecs_assert(table->_ != NULL, ECS_INTERNAL_ERROR, NULL);
        ecs_switch_t *sw = &table->_->sw_columns[u_index];
        data = ecs_vec_first(flecs_switch_values(sw));
        goto has_data;
    }

no_data:
    if (ptr_out) ptr_out[0] = NULL;
    return false;
}

void flecs_iter_populate_data(
    ecs_world_t *world,
    ecs_iter_t *it,
    ecs_table_t *table,
    int32_t offset,
    int32_t count,
    void **ptrs)
{
    ecs_table_t *prev_table = it->table;
    if (prev_table) {
        it->frame_offset += ecs_table_count(prev_table);
    }

    it->table = table;
    it->offset = offset;
    it->count = count;
    if (table) {
        ecs_assert(count != 0 || !ecs_table_count(table) || (it->flags & EcsIterTableOnly), 
            ECS_INTERNAL_ERROR, NULL);
        if (count) {
            it->entities = ecs_vec_get_t(
                &table->data.entities, ecs_entity_t, offset);
        } else {
            it->entities = NULL;
        }
    }

    int t, field_count = it->field_count;
    if (ECS_BIT_IS_SET(it->flags, EcsIterNoData)) {
        ECS_BIT_CLEAR(it->flags, EcsIterHasShared);
        return;
    }

    bool has_shared = false;
    if (ptrs) {
        for (t = 0; t < field_count; t ++) {
            int32_t column = it->columns[t];
            has_shared |= flecs_iter_populate_term_data(world, it, t, column,
                &ptrs[t]);
        }
    }

    ECS_BIT_COND(it->flags, EcsIterHasShared, has_shared);
}

bool flecs_iter_next_row(
    ecs_iter_t *it)
{
    ecs_assert(it != NULL, ECS_INTERNAL_ERROR, NULL);

    bool is_instanced = ECS_BIT_IS_SET(it->flags, EcsIterIsInstanced);
    if (!is_instanced) {
        int32_t instance_count = it->instance_count;
        int32_t count = it->count;
        int32_t offset = it->offset;

        if (instance_count > count && offset < (instance_count - 1)) {
            ecs_assert(count == 1, ECS_INTERNAL_ERROR, NULL);
            int t, field_count = it->field_count;

            for (t = 0; t < field_count; t ++) {
                int32_t column = it->columns[t];
                if (column >= 0) {
                    void *ptr = it->ptrs[t];
                    if (ptr) {
                        it->ptrs[t] = ECS_OFFSET(ptr, it->sizes[t]);
                    }
                }
            }

            if (it->entities) {
                it->entities ++;
            }
            it->offset ++;

            return true;
        }
    }

    return false;
}

bool flecs_iter_next_instanced(
    ecs_iter_t *it,
    bool result)
{
    it->instance_count = it->count;
    bool is_instanced = ECS_BIT_IS_SET(it->flags, EcsIterIsInstanced);
    bool has_shared = ECS_BIT_IS_SET(it->flags, EcsIterHasShared);
    if (result && !is_instanced && it->count && has_shared) {
        it->count = 1;
    }

    return result;
}

/* --- Public API --- */

void* ecs_field_w_size(
    const ecs_iter_t *it,
    size_t size,
    int32_t term)
{
    ecs_check(it->flags & EcsIterIsValid, ECS_INVALID_PARAMETER, NULL);
    ecs_check(it->ptrs != NULL, ECS_INVALID_PARAMETER, NULL);
    ecs_check(!size || ecs_field_size(it, term) == size || 
        (!ecs_field_size(it, term) && (!it->ptrs[term - 1])), 
            ECS_INVALID_PARAMETER, NULL);
    (void)size;

    if (!term) {
        return it->entities;
    }

    return it->ptrs[term - 1];
error:
    return NULL;
}

bool ecs_field_is_readonly(
    const ecs_iter_t *it,
    int32_t term_index)
{
    ecs_check(it->flags & EcsIterIsValid, ECS_INVALID_PARAMETER, NULL);
    ecs_check(term_index > 0, ECS_INVALID_PARAMETER, NULL);

    ecs_term_t *term = &it->terms[term_index - 1];
    ecs_check(term != NULL, ECS_INVALID_PARAMETER, NULL);
    
    if (term->inout == EcsIn) {
        return true;
    } else {
        ecs_term_id_t *src = &term->src;

        if (term->inout == EcsInOutDefault) {
            if (!(ecs_term_match_this(term))) {
                return true;
            }

            if (!(src->flags & EcsSelf)) {
                return true;
            }
        }
    }

error:
    return false;
}

bool ecs_field_is_writeonly(
    const ecs_iter_t *it,
    int32_t term_index)
{
    ecs_check(it->flags & EcsIterIsValid, ECS_INVALID_PARAMETER, NULL);
    ecs_check(term_index > 0, ECS_INVALID_PARAMETER, NULL);

    ecs_term_t *term = &it->terms[term_index - 1];
    ecs_check(term != NULL, ECS_INVALID_PARAMETER, NULL);
    
    if (term->inout == EcsOut) {
        return true;
    }

error:
    return false;
}

bool ecs_field_is_set(
    const ecs_iter_t *it,
    int32_t index)
{
    ecs_check(it->flags & EcsIterIsValid, ECS_INVALID_PARAMETER, NULL);

    int32_t column = it->columns[index - 1];
    if (!column) {
        return false;
    } else if (column < 0) {
        if (it->references) {
            column = -column - 1;
            ecs_ref_t *ref = &it->references[column];
            return ref->entity != 0;
        } else {
            return true;
        }
    }

    return true;
error:
    return false;
}

bool ecs_field_is_self(
    const ecs_iter_t *it,
    int32_t index)
{
    ecs_assert(it != NULL, ECS_INVALID_PARAMETER, NULL);
    ecs_assert(index >= 0, ECS_INVALID_PARAMETER, NULL);
    return it->sources == NULL || it->sources[index - 1] == 0;
}

ecs_id_t ecs_field_id(
    const ecs_iter_t *it,
    int32_t index)
{
    ecs_assert(it != NULL, ECS_INVALID_PARAMETER, NULL);
    ecs_assert(index >= 0, ECS_INVALID_PARAMETER, NULL);
    return it->ids[index - 1];
}

int32_t ecs_field_column_index(
    const ecs_iter_t *it,
    int32_t index)
{
    ecs_assert(it != NULL, ECS_INVALID_PARAMETER, NULL);
    ecs_assert(index >= 0, ECS_INVALID_PARAMETER, NULL);

    int32_t result = it->columns[index - 1];
    if (result <= 0) {
        return -1;
    }

    return result - 1;
}

ecs_entity_t ecs_field_src(
    const ecs_iter_t *it,
    int32_t index)
{
    ecs_assert(it != NULL, ECS_INVALID_PARAMETER, NULL);
    ecs_assert(index >= 0, ECS_INVALID_PARAMETER, NULL);
    if (it->sources) {
        return it->sources[index - 1];
    } else {
        return 0;
    }
}

size_t ecs_field_size(
    const ecs_iter_t *it,
    int32_t index)
{
    ecs_assert(it != NULL, ECS_INVALID_PARAMETER, NULL);
    ecs_assert(index >= 0, ECS_INVALID_PARAMETER, NULL);

    if (index == 0) {
        return sizeof(ecs_entity_t);
    } else {
        return (size_t)it->sizes[index - 1];
    }
}

char* ecs_iter_str(
    const ecs_iter_t *it)
{
    ecs_world_t *world = it->world;
    ecs_strbuf_t buf = ECS_STRBUF_INIT;
    int i;

    if (it->field_count) {
        ecs_strbuf_list_push(&buf, "id:  ", ",");
        for (i = 0; i < it->field_count; i ++) {
            ecs_id_t id = ecs_field_id(it, i + 1);
            char *str = ecs_id_str(world, id);
            ecs_strbuf_list_appendstr(&buf, str);
            ecs_os_free(str);
        }
        ecs_strbuf_list_pop(&buf, "\n");

        ecs_strbuf_list_push(&buf, "src: ", ",");
        for (i = 0; i < it->field_count; i ++) {
            ecs_entity_t subj = ecs_field_src(it, i + 1);
            char *str = ecs_get_fullpath(world, subj);
            ecs_strbuf_list_appendstr(&buf, str);
            ecs_os_free(str);
        }
        ecs_strbuf_list_pop(&buf, "\n");

        ecs_strbuf_list_push(&buf, "set: ", ",");
        for (i = 0; i < it->field_count; i ++) {
            if (ecs_field_is_set(it, i + 1)) {
                ecs_strbuf_list_appendlit(&buf, "true");
            } else {
                ecs_strbuf_list_appendlit(&buf, "false");
            }
        }
        ecs_strbuf_list_pop(&buf, "\n");
    }

    if (it->variable_count) {
        int32_t actual_count = 0;
        for (i = 0; i < it->variable_count; i ++) {
            const char *var_name = it->variable_names[i];
            if (!var_name || var_name[0] == '_' || !strcmp(var_name, "This")) {
                /* Skip anonymous variables */
                continue;
            }

            ecs_var_t var = it->variables[i];
            if (!var.entity) {
                /* Skip table variables */
                continue;
            }

            if (!actual_count) {
                ecs_strbuf_list_push(&buf, "var: ", ",");
            }

            char *str = ecs_get_fullpath(world, var.entity);
            ecs_strbuf_list_append(&buf, "%s=%s", var_name, str);
            ecs_os_free(str);

            actual_count ++;
        }
        if (actual_count) {
            ecs_strbuf_list_pop(&buf, "\n");
        }
    }

    if (it->count) {
        ecs_strbuf_appendlit(&buf, "this:\n");
        for (i = 0; i < it->count; i ++) {
            ecs_entity_t e = it->entities[i];
            char *str = ecs_get_fullpath(world, e);
            ecs_strbuf_appendlit(&buf, "    - ");
            ecs_strbuf_appendstr(&buf, str);
            ecs_strbuf_appendch(&buf, '\n');
            ecs_os_free(str);
        }
    }

    return ecs_strbuf_get(&buf);
}

void ecs_iter_poly(
    const ecs_world_t *world,
    const ecs_poly_t *poly,
    ecs_iter_t *iter_out,
    ecs_term_t *filter)
{
    ecs_iterable_t *iterable = ecs_get_iterable(poly);
    iterable->init(world, poly, iter_out, filter);
}

bool ecs_iter_next(
    ecs_iter_t *iter)
{
    ecs_check(iter != NULL, ECS_INVALID_PARAMETER, NULL);
    ecs_check(iter->next != NULL, ECS_INVALID_PARAMETER, NULL);
    return iter->next(iter);
error:
    return false;
}

int32_t ecs_iter_count(
    ecs_iter_t *it)
{
    ecs_check(it != NULL, ECS_INVALID_PARAMETER, NULL);

    ECS_BIT_SET(it->flags, EcsIterNoData);
    ECS_BIT_SET(it->flags, EcsIterIsInstanced);

    int32_t count = 0;
    while (ecs_iter_next(it)) {
        count += it->count;
    }
    return count;
error:
    return 0;
}

ecs_entity_t ecs_iter_first(
    ecs_iter_t *it)
{
    ecs_check(it != NULL, ECS_INVALID_PARAMETER, NULL);

    ECS_BIT_SET(it->flags, EcsIterNoData);
    ECS_BIT_SET(it->flags, EcsIterIsInstanced);

    ecs_entity_t result = 0;
    if (ecs_iter_next(it)) {
        result = it->entities[0];
        ecs_iter_fini(it);
    }

    return result;
error:
    return 0;
}

bool ecs_iter_is_true(
    ecs_iter_t *it)
{
    ecs_check(it != NULL, ECS_INVALID_PARAMETER, NULL);

    ECS_BIT_SET(it->flags, EcsIterNoData);
    ECS_BIT_SET(it->flags, EcsIterIsInstanced);

    bool result = ecs_iter_next(it);
    if (result) {
        ecs_iter_fini(it);
    }
    return result;
error:
    return false;
}

ecs_entity_t ecs_iter_get_var(
    ecs_iter_t *it,
    int32_t var_id)
{
    ecs_check(var_id >= 0, ECS_INVALID_PARAMETER, NULL);
    ecs_check(var_id < it->variable_count, ECS_INVALID_PARAMETER, NULL);
    ecs_check(it->variables != NULL, ECS_INVALID_PARAMETER, NULL);

    ecs_var_t *var = &it->variables[var_id];
    ecs_entity_t e = var->entity;
    if (!e) {
        ecs_table_t *table = var->range.table;
        if (table) {
            if ((var->range.count == 1) || (ecs_table_count(table) == 1)) {
                ecs_assert(ecs_table_count(table) > var->range.offset,
                    ECS_INTERNAL_ERROR, NULL);
                e = ecs_vec_get_t(&table->data.entities, ecs_entity_t,
                    var->range.offset)[0];
            }
        }
    } else {
        ecs_assert(ecs_is_valid(it->real_world, e), ECS_INTERNAL_ERROR, NULL);
    }

    return e;
error:
    return 0;
}

ecs_table_t* ecs_iter_get_var_as_table(
    ecs_iter_t *it,
    int32_t var_id)
{
    ecs_check(var_id >= 0, ECS_INVALID_PARAMETER, NULL);
    ecs_check(var_id < it->variable_count, ECS_INVALID_PARAMETER, NULL);
    ecs_check(it->variables != NULL, ECS_INVALID_PARAMETER, NULL);

    ecs_var_t *var = &it->variables[var_id];
    ecs_table_t *table = var->range.table;
    if (!table) {
        /* If table is not set, try to get table from entity */
        ecs_entity_t e = var->entity;
        if (e) {
            ecs_record_t *r = flecs_entities_get(it->real_world, e);
            if (r) {
                table = r->table;
                if (ecs_table_count(table) != 1) {
                    /* If table contains more than the entity, make sure not to
                     * return a partial table. */
                    return NULL;
                }
            }
        }
    }

    if (table) {
        if (var->range.offset) {
            /* Don't return whole table if only partial table is matched */
            return NULL;
        }

        if (!var->range.count || ecs_table_count(table) == var->range.count) {
            /* Return table if count matches */
            return table;
        }
    }

error:
    return NULL;
}

ecs_table_range_t ecs_iter_get_var_as_range(
    ecs_iter_t *it,
    int32_t var_id)
{
    ecs_check(var_id >= 0, ECS_INVALID_PARAMETER, NULL);
    ecs_check(var_id < it->variable_count, ECS_INVALID_PARAMETER, NULL);
    ecs_check(it->variables != NULL, ECS_INVALID_PARAMETER, NULL);

    ecs_table_range_t result = { 0 };

    ecs_var_t *var = &it->variables[var_id];
    ecs_table_t *table = var->range.table;
    if (!table) {
        ecs_entity_t e = var->entity;
        if (e) {
            ecs_record_t *r = flecs_entities_get(it->real_world, e);
            if (r) {
                result.table = r->table;
                result.offset = ECS_RECORD_TO_ROW(r->row);
                result.count = 1;
            }
        }
    } else {
        result.table = table;
        result.offset = var->range.offset;
        result.count = var->range.count;
        if (!result.count) {
            result.count = ecs_table_count(table);
        }
    }

    return result;
error:
    return (ecs_table_range_t){0};
}

void ecs_iter_set_var(
    ecs_iter_t *it,
    int32_t var_id,
    ecs_entity_t entity)
{
    ecs_check(it != NULL, ECS_INVALID_PARAMETER, NULL);
    ecs_check(var_id >= 0, ECS_INVALID_PARAMETER, NULL);
    ecs_check(var_id < FLECS_VARIABLE_COUNT_MAX, ECS_INVALID_PARAMETER, NULL);
    ecs_check(var_id < it->variable_count, ECS_INVALID_PARAMETER, NULL);
    ecs_check(entity != 0, ECS_INVALID_PARAMETER, NULL);
    /* Can't set variable while iterating */
    ecs_check(!(it->flags & EcsIterIsValid), ECS_INVALID_PARAMETER, NULL);
    ecs_check(it->variables != NULL, ECS_INTERNAL_ERROR, NULL);

    ecs_var_t *var = &it->variables[var_id];
    var->entity = entity;

    ecs_record_t *r = flecs_entities_get(it->real_world, entity);
    if (r) {
        var->range.table = r->table;
        var->range.offset = ECS_RECORD_TO_ROW(r->row);
        var->range.count = 1;
    } else {
        var->range.table = NULL;
        var->range.offset = 0;
        var->range.count = 0;
    }

    it->constrained_vars |= flecs_ito(uint64_t, 1 << var_id);

error:
    return;
}

void ecs_iter_set_var_as_table(
    ecs_iter_t *it,
    int32_t var_id,
    const ecs_table_t *table)
{
    ecs_table_range_t range = { .table = (ecs_table_t*)table };
    ecs_iter_set_var_as_range(it, var_id, &range);
}

void ecs_iter_set_var_as_range(
    ecs_iter_t *it,
    int32_t var_id,
    const ecs_table_range_t *range)
{
    ecs_check(it != NULL, ECS_INVALID_PARAMETER, NULL);
    ecs_check(var_id >= 0, ECS_INVALID_PARAMETER, NULL);
    ecs_check(var_id < FLECS_VARIABLE_COUNT_MAX, ECS_INVALID_PARAMETER, NULL);
    ecs_check(var_id < it->variable_count, ECS_INVALID_PARAMETER, NULL);
    ecs_check(range != 0, ECS_INVALID_PARAMETER, NULL);
    ecs_check(range->table != NULL, ECS_INVALID_PARAMETER, NULL);
    ecs_check(!range->offset || range->offset < ecs_table_count(range->table), 
        ECS_INVALID_PARAMETER, NULL);
    ecs_check((range->offset + range->count) <= ecs_table_count(range->table), 
        ECS_INVALID_PARAMETER, NULL);

    /* Can't set variable while iterating */
    ecs_check(!(it->flags & EcsIterIsValid), ECS_INVALID_OPERATION, NULL);

    ecs_var_t *var = &it->variables[var_id];
    var->range = *range;

    if (range->count == 1) {
        ecs_table_t *table = range->table;
        var->entity = ecs_vec_get_t(
            &table->data.entities, ecs_entity_t, range->offset)[0];
    } else {
        var->entity = 0;
    }

    it->constrained_vars |= flecs_uto(uint64_t, 1 << var_id);

error:
    return;
}

bool ecs_iter_var_is_constrained(
    ecs_iter_t *it,
    int32_t var_id)
{
    return (it->constrained_vars & (flecs_uto(uint64_t, 1 << var_id))) != 0;
}

static
void ecs_chained_iter_fini(
    ecs_iter_t *it)
{
    ecs_assert(it != NULL, ECS_INVALID_PARAMETER, NULL);
    ecs_assert(it->chain_it != NULL, ECS_INVALID_PARAMETER, NULL);

    ecs_iter_fini(it->chain_it);

    it->chain_it = NULL;
}

ecs_iter_t ecs_page_iter(
    const ecs_iter_t *it,
    int32_t offset,
    int32_t limit)
{
    ecs_check(it != NULL, ECS_INVALID_PARAMETER, NULL);
    ecs_check(it->next != NULL, ECS_INVALID_PARAMETER, NULL);

    ecs_iter_t result = *it;
    result.priv.iter.page = (ecs_page_iter_t){
        .offset = offset,
        .limit = limit,
        .remaining = limit
    };
    result.next = ecs_page_next;
    result.fini = ecs_chained_iter_fini;
    result.chain_it = (ecs_iter_t*)it;

    return result;
error:
    return (ecs_iter_t){ 0 };
}

static
void flecs_offset_iter(
    ecs_iter_t *it,
    int32_t offset)
{
    it->entities = &it->entities[offset];

    int32_t t, field_count = it->field_count;
    void **it_ptrs = it->ptrs;
    if (it_ptrs) {
        for (t = 0; t < field_count; t ++) {
            void *ptrs = it_ptrs[t];
            if (!ptrs) {
                continue;
            }

            if (it->sources[t]) {
                continue;
            }

            it->ptrs[t] = ECS_OFFSET(ptrs, offset * it->sizes[t]);
        }
    }
}

static
bool ecs_page_next_instanced(
    ecs_iter_t *it)
{
    ecs_check(it != NULL, ECS_INVALID_PARAMETER, NULL);
    ecs_check(it->chain_it != NULL, ECS_INVALID_PARAMETER, NULL);
    ecs_check(it->next == ecs_page_next, ECS_INVALID_PARAMETER, NULL);

    ecs_iter_t *chain_it = it->chain_it;
    bool instanced = ECS_BIT_IS_SET(it->flags, EcsIterIsInstanced);

    do {
        if (!ecs_iter_next(chain_it)) {
            goto depleted;
        }

        ecs_page_iter_t *iter = &it->priv.iter.page;
        
        /* Copy everything up to the private iterator data */
        ecs_os_memcpy(it, chain_it, offsetof(ecs_iter_t, priv));

        /* Keep instancing setting from original iterator */
        ECS_BIT_COND(it->flags, EcsIterIsInstanced, instanced);

        if (!chain_it->table) {
            goto yield; /* Task query */
        }

        int32_t offset = iter->offset;
        int32_t limit = iter->limit;
        if (!(offset || limit)) {
            if (it->count) {
                goto yield;
            } else {
                goto depleted;
            }
        }

        int32_t count = it->count;
        int32_t remaining = iter->remaining;

        if (offset) {
            if (offset > count) {
                /* No entities to iterate in current table */
                iter->offset -= count;
                it->count = 0;
                continue;
            } else {
                it->offset += offset;
                count = it->count -= offset;
                iter->offset = 0;
                flecs_offset_iter(it, offset);
            }
        }

        if (remaining) {
            if (remaining > count) {
                iter->remaining -= count;
            } else {
                it->count = remaining;
                iter->remaining = 0;
            }
        } else if (limit) {
            /* Limit hit: no more entities left to iterate */
            goto done;
        }
    } while (it->count == 0);

yield:
    if (!ECS_BIT_IS_SET(it->flags, EcsIterIsInstanced)) {
        it->offset = 0;
    }

    return true;
done:
    /* Cleanup iterator resources if it wasn't yet depleted */
    ecs_iter_fini(chain_it);
depleted:
error:
    return false;
}

bool ecs_page_next(
    ecs_iter_t *it)
{
    ecs_check(it != NULL, ECS_INVALID_PARAMETER, NULL);
    ecs_check(it->next == ecs_page_next, ECS_INVALID_PARAMETER, NULL);
    ecs_check(it->chain_it != NULL, ECS_INVALID_PARAMETER, NULL);

    ECS_BIT_SET(it->chain_it->flags, EcsIterIsInstanced);

    if (flecs_iter_next_row(it)) {
        return true;
    }

    return flecs_iter_next_instanced(it, ecs_page_next_instanced(it));
error:
    return false;
}

ecs_iter_t ecs_worker_iter(
    const ecs_iter_t *it,
    int32_t index,
    int32_t count)
{
    ecs_check(it != NULL, ECS_INVALID_PARAMETER, NULL);
    ecs_check(it->next != NULL, ECS_INVALID_PARAMETER, NULL);
    ecs_check(count > 0, ECS_INVALID_PARAMETER, NULL);
    ecs_check(index >= 0, ECS_INVALID_PARAMETER, NULL);
    ecs_check(index < count, ECS_INVALID_PARAMETER, NULL);

    ecs_iter_t result = *it;
    result.priv.iter.worker = (ecs_worker_iter_t){
        .index = index,
        .count = count
    };
    result.next = ecs_worker_next;
    result.fini = ecs_chained_iter_fini;
    result.chain_it = (ecs_iter_t*)it;

    return result;
error:
    return (ecs_iter_t){ 0 };
}

static
bool ecs_worker_next_instanced(
    ecs_iter_t *it)
{
    ecs_check(it != NULL, ECS_INVALID_PARAMETER, NULL);
    ecs_check(it->chain_it != NULL, ECS_INVALID_PARAMETER, NULL);
    ecs_check(it->next == ecs_worker_next, ECS_INVALID_PARAMETER, NULL);

    bool instanced = ECS_BIT_IS_SET(it->flags, EcsIterIsInstanced);

    ecs_iter_t *chain_it = it->chain_it;
    ecs_worker_iter_t *iter = &it->priv.iter.worker;
    int32_t res_count = iter->count, res_index = iter->index;
    int32_t per_worker, instances_per_worker, first;

    do {
        if (!ecs_iter_next(chain_it)) {
            return false;
        }

        /* Copy everything up to the private iterator data */
        ecs_os_memcpy(it, chain_it, offsetof(ecs_iter_t, priv));

        /* Keep instancing setting from original iterator */
        ECS_BIT_COND(it->flags, EcsIterIsInstanced, instanced);

        int32_t count = it->count;
        int32_t instance_count = it->instance_count;
        per_worker = count / res_count;
        instances_per_worker = instance_count / res_count;
        first = per_worker * res_index;
        count -= per_worker * res_count;

        if (count) {
            if (res_index < count) {
                per_worker ++;
                first += res_index;
            } else {
                first += count;
            }
        }

        if (!per_worker && it->table == NULL) {
            if (res_index == 0) {
                return true;
            } else {
                return false;
            }
        }
    } while (!per_worker);

    it->instance_count = instances_per_worker;
    it->frame_offset += first;

    flecs_offset_iter(it, it->offset + first);
    it->count = per_worker;

    if (ECS_BIT_IS_SET(it->flags, EcsIterIsInstanced)) {
        it->offset += first;
    } else {
        it->offset = 0;
    }

    return true;
error:
    return false;
}

bool ecs_worker_next(
    ecs_iter_t *it)
{
    ecs_check(it != NULL, ECS_INVALID_PARAMETER, NULL);
    ecs_check(it->next == ecs_worker_next, ECS_INVALID_PARAMETER, NULL);
    ecs_check(it->chain_it != NULL, ECS_INVALID_PARAMETER, NULL);

    ECS_BIT_SET(it->chain_it->flags, EcsIterIsInstanced);

    if (flecs_iter_next_row(it)) {
        return true;
    }

    return flecs_iter_next_instanced(it, ecs_worker_next_instanced(it));
error:
    return false;
}

/**
 * @file misc.c
 * @brief Miscallaneous functions.
 */

#include <time.h>
#include <ctype.h>

#ifndef FLECS_NDEBUG
static int64_t flecs_s_min[] = { 
    [1] = INT8_MIN, [2] = INT16_MIN, [4] = INT32_MIN, [8] = INT64_MIN };
static int64_t flecs_s_max[] = { 
    [1] = INT8_MAX, [2] = INT16_MAX, [4] = INT32_MAX, [8] = INT64_MAX };
static uint64_t flecs_u_max[] = { 
    [1] = UINT8_MAX, [2] = UINT16_MAX, [4] = UINT32_MAX, [8] = UINT64_MAX };

uint64_t _flecs_ito(
    size_t size,
    bool is_signed,
    bool lt_zero,
    uint64_t u,
    const char *err)
{
    union {
        uint64_t u;
        int64_t s;
    } v;

    v.u = u;

    if (is_signed) {
        ecs_assert(v.s >= flecs_s_min[size], ECS_INVALID_CONVERSION, err);
        ecs_assert(v.s <= flecs_s_max[size], ECS_INVALID_CONVERSION, err);
    } else {
        ecs_assert(lt_zero == false, ECS_INVALID_CONVERSION, err);
        ecs_assert(u <= flecs_u_max[size], ECS_INVALID_CONVERSION, err);
    }

    return u;
}
#endif

int32_t flecs_next_pow_of_2(
    int32_t n)
{
    n --;
    n |= n >> 1;
    n |= n >> 2;
    n |= n >> 4;
    n |= n >> 8;
    n |= n >> 16;
    n ++;

    return n;
}

/** Convert time to double */
double ecs_time_to_double(
    ecs_time_t t)
{
    double result;
    result = t.sec;
    return result + (double)t.nanosec / (double)1000000000;
}

ecs_time_t ecs_time_sub(
    ecs_time_t t1,
    ecs_time_t t2)
{
    ecs_time_t result;

    if (t1.nanosec >= t2.nanosec) {
        result.nanosec = t1.nanosec - t2.nanosec;
        result.sec = t1.sec - t2.sec;
    } else {
        result.nanosec = t1.nanosec - t2.nanosec + 1000000000;
        result.sec = t1.sec - t2.sec - 1;
    }

    return result;
}

void ecs_sleepf(
    double t)
{
    if (t > 0) {
        int sec = (int)t;
        int nsec = (int)((t - sec) * 1000000000);
        ecs_os_sleep(sec, nsec);
    }
}

double ecs_time_measure(
    ecs_time_t *start)
{
    ecs_time_t stop, temp;
    ecs_os_get_time(&stop);
    temp = stop;
    stop = ecs_time_sub(stop, *start);
    *start = temp;
    return ecs_time_to_double(stop);
}

void* ecs_os_memdup(
    const void *src, 
    ecs_size_t size) 
{
    if (!src) {
        return NULL;
    }
        
    void *dst = ecs_os_malloc(size);
    ecs_assert(dst != NULL, ECS_OUT_OF_MEMORY, NULL);
    ecs_os_memcpy(dst, src, size);  
    return dst;  
}

int flecs_entity_compare(
    ecs_entity_t e1, 
    const void *ptr1, 
    ecs_entity_t e2, 
    const void *ptr2) 
{
    (void)ptr1;
    (void)ptr2;
    return (e1 > e2) - (e1 < e2);
}

int flecs_entity_compare_qsort(
    const void *e1,
    const void *e2)
{
    ecs_entity_t v1 = *(ecs_entity_t*)e1;
    ecs_entity_t v2 = *(ecs_entity_t*)e2;
    return flecs_entity_compare(v1, NULL, v2, NULL);
}

uint64_t flecs_string_hash(
    const void *ptr)
{
    const ecs_hashed_string_t *str = ptr;
    ecs_assert(str->hash != 0, ECS_INTERNAL_ERROR, NULL);
    return str->hash;
}

char* ecs_vasprintf(
    const char *fmt,
    va_list args)
{
    ecs_size_t size = 0;
    char *result  = NULL;
    va_list tmpa;

    va_copy(tmpa, args);

    size = vsnprintf(result, 0, fmt, tmpa);

    va_end(tmpa);

    if ((int32_t)size < 0) { 
        return NULL; 
    }

    result = (char *) ecs_os_malloc(size + 1);

    if (!result) { 
        return NULL; 
    }

    ecs_os_vsprintf(result, fmt, args);

    return result;
}

char* ecs_asprintf(
    const char *fmt,
    ...)
{
    va_list args;
    va_start(args, fmt);
    char *result = ecs_vasprintf(fmt, args);
    va_end(args);
    return result;
}

char* flecs_to_snake_case(const char *str) {
    int32_t upper_count = 0, len = 1;
    const char *ptr = str;
    char ch, *out, *out_ptr;

    for (ptr = &str[1]; (ch = *ptr); ptr ++) {
        if (isupper(ch)) {
            upper_count ++;
        }
        len ++;
    }

    out = out_ptr = ecs_os_malloc_n(char, len + upper_count + 1);
    for (ptr = str; (ch = *ptr); ptr ++) {
        if (isupper(ch)) {
            if ((ptr != str) && (out_ptr[-1] != '_')) {
                out_ptr[0] = '_';
                out_ptr ++;
            }
            out_ptr[0] = (char)tolower(ch);
            out_ptr ++;
        } else {
            out_ptr[0] = ch;
            out_ptr ++;
        }
    }

    out_ptr[0] = '\0';

    return out;
}

/**
 * @file value.c
 * @brief Utility functions to work with non-trivial pointers of user types.
 */


int ecs_value_init_w_type_info(
    const ecs_world_t *world,
    const ecs_type_info_t *ti,
    void *ptr)
{
    ecs_poly_assert(world, ecs_world_t);
    ecs_check(ti != NULL, ECS_INVALID_PARAMETER, NULL);
    (void)world;

    ecs_xtor_t ctor;
    if ((ctor = ti->hooks.ctor)) {
        ctor(ptr, 1, ti);
    } else {
        ecs_os_memset(ptr, 0, ti->size);
    }

    return 0;
error:
    return -1;
}

int ecs_value_init(
    const ecs_world_t *world,
    ecs_entity_t type,
    void *ptr)
{
    ecs_poly_assert(world, ecs_world_t);
    const ecs_type_info_t *ti = ecs_get_type_info(world, type);
    ecs_check(ti != NULL, ECS_INVALID_PARAMETER, "entity is not a type");
    return ecs_value_init_w_type_info(world, ti, ptr);
error:
    return -1;
}

void* ecs_value_new_w_type_info(
    ecs_world_t *world,
    const ecs_type_info_t *ti)
{
    ecs_poly_assert(world, ecs_world_t);
    ecs_check(ti != NULL, ECS_INVALID_PARAMETER, NULL);
    (void)world;

    void *result = flecs_alloc(&world->allocator, ti->size);
    if (ecs_value_init_w_type_info(world, ti, result) != 0) {
        flecs_free(&world->allocator, ti->size, result);
        goto error;
    }

    return result;
error:
    return NULL;
}

void* ecs_value_new(
    ecs_world_t *world,
    ecs_entity_t type)
{
    ecs_poly_assert(world, ecs_world_t);
    const ecs_type_info_t *ti = ecs_get_type_info(world, type);
    ecs_check(ti != NULL, ECS_INVALID_PARAMETER, "entity is not a type");

    return ecs_value_new_w_type_info(world, ti);
error:
    return NULL;
}

int ecs_value_fini_w_type_info(
    const ecs_world_t *world,
    const ecs_type_info_t *ti,
    void *ptr)
{
    ecs_poly_assert(world, ecs_world_t);
    ecs_check(ti != NULL, ECS_INVALID_PARAMETER, NULL);
    (void)world;

    ecs_xtor_t dtor;
    if ((dtor = ti->hooks.dtor)) {
        dtor(ptr, 1, ti);
    }

    return 0;
error:
    return -1;
}

int ecs_value_fini(
    const ecs_world_t *world,
    ecs_entity_t type,
    void* ptr)
{
    ecs_poly_assert(world, ecs_world_t);
    (void)world;
    const ecs_type_info_t *ti = ecs_get_type_info(world, type);
    ecs_check(ti != NULL, ECS_INVALID_PARAMETER, "entity is not a type");
    return ecs_value_fini_w_type_info(world, ti, ptr);
error:
    return -1;
}

int ecs_value_free(
    ecs_world_t *world,
    ecs_entity_t type,
    void* ptr)
{
    ecs_poly_assert(world, ecs_world_t);
    const ecs_type_info_t *ti = ecs_get_type_info(world, type);
    ecs_check(ti != NULL, ECS_INVALID_PARAMETER, "entity is not a type");
    if (ecs_value_fini_w_type_info(world, ti, ptr) != 0) {
        goto error;
    }

    flecs_free(&world->allocator, ti->size, ptr);

    return 0;
error:
    return -1;
}

int ecs_value_copy_w_type_info(
    const ecs_world_t *world,
    const ecs_type_info_t *ti,
    void* dst,
    const void *src)
{
    ecs_poly_assert(world, ecs_world_t);
    ecs_check(ti != NULL, ECS_INVALID_PARAMETER, NULL);
    (void)world;

    ecs_copy_t copy;
    if ((copy = ti->hooks.copy)) {
        copy(dst, src, 1, ti);
    } else {
        ecs_os_memcpy(dst, src, ti->size);
    }

    return 0;
error:
    return -1;
}

int ecs_value_copy(
    const ecs_world_t *world,
    ecs_entity_t type,
    void* dst,
    const void *src)
{
    ecs_poly_assert(world, ecs_world_t);
    const ecs_type_info_t *ti = ecs_get_type_info(world, type);
    ecs_check(ti != NULL, ECS_INVALID_PARAMETER, "entity is not a type");
    return ecs_value_copy_w_type_info(world, ti, dst, src);
error:
    return -1;
}

int ecs_value_move_w_type_info(
    const ecs_world_t *world,
    const ecs_type_info_t *ti,
    void* dst,
    void *src)
{
    ecs_poly_assert(world, ecs_world_t);
    ecs_check(ti != NULL, ECS_INVALID_PARAMETER, NULL);
    (void)world;

    ecs_move_t move;
    if ((move = ti->hooks.move)) {
        move(dst, src, 1, ti);
    } else {
        ecs_os_memcpy(dst, src, ti->size);
    }

    return 0;
error:
    return -1;
}

int ecs_value_move(
    const ecs_world_t *world,
    ecs_entity_t type,
    void* dst,
    void *src)
{
    ecs_poly_assert(world, ecs_world_t);
    const ecs_type_info_t *ti = ecs_get_type_info(world, type);
    ecs_check(ti != NULL, ECS_INVALID_PARAMETER, "entity is not a type");
    return ecs_value_move_w_type_info(world, ti, dst, src);
error:
    return -1;
}

int ecs_value_move_ctor_w_type_info(
    const ecs_world_t *world,
    const ecs_type_info_t *ti,
    void* dst,
    void *src)
{
    ecs_poly_assert(world, ecs_world_t);
    ecs_check(ti != NULL, ECS_INVALID_PARAMETER, NULL);
    (void)world;

    ecs_move_t move;
    if ((move = ti->hooks.move_ctor)) {
        move(dst, src, 1, ti);
    } else {
        ecs_os_memcpy(dst, src, ti->size);
    }

    return 0;
error:
    return -1;
}

int ecs_value_move_ctor(
    const ecs_world_t *world,
    ecs_entity_t type,
    void* dst,
    void *src)
{
    ecs_poly_assert(world, ecs_world_t);
    const ecs_type_info_t *ti = ecs_get_type_info(world, type);
    ecs_check(ti != NULL, ECS_INVALID_PARAMETER, "entity is not a type");
    return ecs_value_move_w_type_info(world, ti, dst, src);
error:
    return -1;
}

/**
 * @file bootstrap.c
 * @brief Bootstrap entities in the flecs.core namespace.
 * 
 * Before the ECS storage can be used, core entities such first need to be 
 * initialized. For example, components in Flecs are stored as entities in the
 * ECS storage itself with an EcsComponent component, but before this component
 * can be stored, the component itself needs to be initialized.
 * 
 * The bootstrap code uses lower-level APIs to initialize the data structures.
 * After bootstrap is completed, regular ECS operations can be used to create
 * entities and components.
 * 
 * The bootstrap file also includes several lifecycle hooks and observers for
 * builtin features, such as relationship properties and hooks for keeping the
 * entity name administration in sync with the (Identifier, Name) component.
 */


/* -- Identifier Component -- */
static ECS_DTOR(EcsIdentifier, ptr, {
    ecs_os_strset(&ptr->value, NULL);
})

static ECS_COPY(EcsIdentifier, dst, src, {
    ecs_os_strset(&dst->value, src->value);
    dst->hash = src->hash;
    dst->length = src->length;
    dst->index_hash = src->index_hash;
    dst->index = src->index;
})

static ECS_MOVE(EcsIdentifier, dst, src, {
    ecs_os_strset(&dst->value, NULL);
    dst->value = src->value;
    dst->hash = src->hash;
    dst->length = src->length;
    dst->index_hash = src->index_hash;
    dst->index = src->index;

    src->value = NULL;
    src->hash = 0;
    src->index_hash = 0;
    src->index = 0;
    src->length = 0;
})

static
void ecs_on_set(EcsIdentifier)(ecs_iter_t *it) {
    EcsIdentifier *ptr = ecs_field(it, EcsIdentifier, 1);
    
    ecs_world_t *world = it->real_world;
    ecs_entity_t evt = it->event;
    ecs_id_t evt_id = it->event_id;
    ecs_entity_t kind = ECS_PAIR_SECOND(evt_id); /* Name, Symbol, Alias */
    ecs_id_t pair = ecs_childof(0);
    ecs_hashmap_t *index = NULL;

    if (kind == EcsSymbol) {
        index = &world->symbols;
    } else if (kind == EcsAlias) {
        index = &world->aliases;
    } else if (kind == EcsName) {
        ecs_assert(it->table != NULL, ECS_INTERNAL_ERROR, NULL);
        ecs_search(world, it->table, ecs_childof(EcsWildcard), &pair);
        ecs_assert(pair != 0, ECS_INTERNAL_ERROR, NULL);

        if (evt == EcsOnSet) {
            index = flecs_id_name_index_ensure(world, pair);
        } else {
            index = flecs_id_name_index_get(world, pair);
        }
    }

    int i, count = it->count;

    for (i = 0; i < count; i ++) {
        EcsIdentifier *cur = &ptr[i];
        uint64_t hash;
        ecs_size_t len;
        const char *name = cur->value;

        if (cur->index && cur->index != index) {
            /* If index doesn't match up, the value must have been copied from
             * another entity, so reset index & cached index hash */
            cur->index = NULL;
            cur->index_hash = 0;
        }

        if (cur->value && (evt == EcsOnSet)) {
            len = cur->length = ecs_os_strlen(name);
            hash = cur->hash = flecs_hash(name, len);
        } else {
            len = cur->length = 0;
            hash = cur->hash = 0;
            cur->index = NULL;
        }

        if (index) {
            uint64_t index_hash = cur->index_hash;
            ecs_entity_t e = it->entities[i];

            if (hash != index_hash) {
                if (index_hash) {
                    flecs_name_index_remove(index, e, index_hash);
                }
                if (hash) {
                    flecs_name_index_ensure(index, e, name, len, hash);
                    cur->index_hash = hash;
                    cur->index = index;
                }
            } else {
                /* Name didn't change, but the string could have been 
                 * reallocated. Make sure name index points to correct string */
                flecs_name_index_update_name(index, e, hash, name);
            }
        }
    }
}


/* -- Poly component -- */

static ECS_COPY(EcsPoly, dst, src, {
    (void)dst;
    (void)src;
    ecs_abort(ECS_INVALID_OPERATION, "poly component cannot be copied");
})

static ECS_MOVE(EcsPoly, dst, src, {
    if (dst->poly && (dst->poly != src->poly)) {
        ecs_poly_dtor_t *dtor = ecs_get_dtor(dst->poly);
        ecs_assert(dtor != NULL, ECS_INTERNAL_ERROR, NULL);
        dtor[0](dst->poly);
    }

    dst->poly = src->poly;
    src->poly = NULL;
})

static ECS_DTOR(EcsPoly, ptr, {
    if (ptr->poly) {
        ecs_poly_dtor_t *dtor = ecs_get_dtor(ptr->poly);
        ecs_assert(dtor != NULL, ECS_INTERNAL_ERROR, NULL);
        dtor[0](ptr->poly);
    }
})


/* -- Builtin triggers -- */

static
void flecs_assert_relation_unused(
    ecs_world_t *world, 
    ecs_entity_t rel,
    ecs_entity_t property)
{
    if (world->flags & (EcsWorldInit|EcsWorldFini)) {
        return;
    }

    ecs_vec_t *marked_ids = &world->store.marked_ids;
    int32_t i, count = ecs_vec_count(marked_ids);
    for (i = 0; i < count; i ++) {
        ecs_marked_id_t *mid = ecs_vec_get_t(marked_ids, ecs_marked_id_t, i);
        if (mid->id == ecs_pair(rel, EcsWildcard)) {
            /* If id is being cleaned up, no need to throw error as tables will
             * be cleaned up */
            return;
        }
    }

    bool in_use = ecs_id_in_use(world, ecs_pair(rel, EcsWildcard));
    if (property != EcsUnion) {
        in_use |= ecs_id_in_use(world, rel);
    }
    if (in_use) {
        char *r_str = ecs_get_fullpath(world, rel);
        char *p_str = ecs_get_fullpath(world, property);

        ecs_throw(ECS_ID_IN_USE, 
            "cannot change property '%s' for relationship '%s': already in use",
            p_str, r_str);
        
        ecs_os_free(r_str);
        ecs_os_free(p_str);
    }

error:
    return;
}

static
bool flecs_set_id_flag(
    ecs_id_record_t *idr, 
    ecs_flags32_t flag)
{
    if (!(idr->flags & flag)) {
        idr->flags |= flag;
        return true;
    }
    return false;
}

static
bool flecs_unset_id_flag(
    ecs_id_record_t *idr, 
    ecs_flags32_t flag)
{
    if ((idr->flags & flag)) {
        idr->flags &= ~flag;
        return true;
    }
    return false;
}

static
void flecs_register_id_flag_for_relation(
    ecs_iter_t *it,
    ecs_entity_t prop,
    ecs_flags32_t flag,
    ecs_flags32_t not_flag,
    ecs_flags32_t entity_flag)
{
    ecs_world_t *world = it->world;
    ecs_entity_t event = it->event;

    int i, count = it->count;
    for (i = 0; i < count; i ++) {
        ecs_entity_t e = it->entities[i];
        bool changed = false;

        if (event == EcsOnAdd) {
            ecs_id_record_t *idr = flecs_id_record_ensure(world, e);
            changed |= flecs_set_id_flag(idr, flag);
            idr = flecs_id_record_ensure(world, ecs_pair(e, EcsWildcard));
            do {
                changed |= flecs_set_id_flag(idr, flag);
            } while ((idr = idr->first.next));
            if (entity_flag) flecs_add_flag(world, e, entity_flag);
        } else if (event == EcsOnRemove) {
            ecs_id_record_t *idr = flecs_id_record_get(world, e);
            if (idr) changed |= flecs_unset_id_flag(idr, not_flag);
            idr = flecs_id_record_get(world, ecs_pair(e, EcsWildcard));
            if (idr) {
                do {
                    changed |= flecs_unset_id_flag(idr, not_flag);
                } while ((idr = idr->first.next));
            }
        }

        if (changed) {
            flecs_assert_relation_unused(world, e, prop);
        }
    }
}

static
void flecs_register_final(ecs_iter_t *it) {
    ecs_world_t *world = it->world;
    
    int i, count = it->count;
    for (i = 0; i < count; i ++) {
        ecs_entity_t e = it->entities[i];
        if (flecs_id_record_get(world, ecs_pair(EcsIsA, e)) != NULL) {
            char *e_str = ecs_get_fullpath(world, e);
            ecs_throw(ECS_ID_IN_USE,
                "cannot change property 'Final' for '%s': already inherited from",
                    e_str);
            ecs_os_free(e_str);
        error:
            continue;
        }
    }
}

static
void flecs_register_on_delete(ecs_iter_t *it) {
    ecs_id_t id = ecs_field_id(it, 1);
    flecs_register_id_flag_for_relation(it, EcsOnDelete, 
        ECS_ID_ON_DELETE_FLAG(ECS_PAIR_SECOND(id)),
        EcsIdOnDeleteMask,
        EcsEntityIsId);
}

static
void flecs_register_on_delete_object(ecs_iter_t *it) {
    ecs_id_t id = ecs_field_id(it, 1);
    flecs_register_id_flag_for_relation(it, EcsOnDeleteTarget, 
        ECS_ID_ON_DELETE_OBJECT_FLAG(ECS_PAIR_SECOND(id)),
        EcsIdOnDeleteObjectMask,
        EcsEntityIsId);  
}

static
void flecs_register_traversable(ecs_iter_t *it) {
    flecs_register_id_flag_for_relation(it, EcsAcyclic, EcsIdTraversable, 
        EcsIdTraversable, 0);
}

static
void flecs_register_tag(ecs_iter_t *it) {
    flecs_register_id_flag_for_relation(it, EcsTag, EcsIdTag, ~EcsIdTag, 0);

    /* Ensure that all id records for tag have type info set to NULL */
    ecs_world_t *world = it->real_world;
    int i, count = it->count;
    for (i = 0; i < count; i ++) {
        ecs_entity_t e = it->entities[i];

        if (it->event == EcsOnAdd) {
            ecs_id_record_t *idr = flecs_id_record_get(world, 
                ecs_pair(e, EcsWildcard));
            ecs_assert(idr != NULL, ECS_INTERNAL_ERROR, NULL);
            do {
                if (idr->type_info != NULL) {
                    flecs_assert_relation_unused(world, e, EcsTag);
                }
                idr->type_info = NULL;
            } while ((idr = idr->first.next));
        }
    }
}

static
void flecs_register_exclusive(ecs_iter_t *it) {
    flecs_register_id_flag_for_relation(it, EcsExclusive, EcsIdExclusive, 
        EcsIdExclusive, 0);
}

static
void flecs_register_dont_inherit(ecs_iter_t *it) {
    flecs_register_id_flag_for_relation(it, EcsDontInherit, 
        EcsIdDontInherit, EcsIdDontInherit, 0);
}

static
void flecs_register_always_override(ecs_iter_t *it) {
    flecs_register_id_flag_for_relation(it, EcsAlwaysOverride, 
        EcsIdAlwaysOverride, EcsIdAlwaysOverride, 0);
}

static
void flecs_register_with(ecs_iter_t *it) {
    flecs_register_id_flag_for_relation(it, EcsWith, EcsIdWith, 0, 0);
}

static
void flecs_register_union(ecs_iter_t *it) {
    flecs_register_id_flag_for_relation(it, EcsUnion, EcsIdUnion, 0, 0);
}

static
void flecs_register_slot_of(ecs_iter_t *it) {
    int i, count = it->count;
    for (i = 0; i < count; i ++) {
        ecs_add_id(it->world, it->entities[i], EcsUnion);
    }
}

static
void flecs_on_symmetric_add_remove(ecs_iter_t *it) {
    ecs_entity_t pair = ecs_field_id(it, 1);

    if (!ECS_HAS_ID_FLAG(pair, PAIR)) {
        /* If relationship was not added as a pair, there's nothing to do */
        return;
    }

    ecs_world_t *world = it->world;
    ecs_entity_t rel = ECS_PAIR_FIRST(pair);
    ecs_entity_t obj = ecs_pair_second(world, pair);
    ecs_entity_t event = it->event;
    
    int i, count = it->count;
    for (i = 0; i < count; i ++) {
        ecs_entity_t subj = it->entities[i];
        if (event == EcsOnAdd) {
            if (!ecs_has_id(it->real_world, obj, ecs_pair(rel, subj))) {
                ecs_add_pair(it->world, obj, rel, subj);   
            }
        } else {
            if (ecs_has_id(it->real_world, obj, ecs_pair(rel, subj))) {
                ecs_remove_pair(it->world, obj, rel, subj);   
            }
        }
    }
}

static
void flecs_register_symmetric(ecs_iter_t *it) {
    ecs_world_t *world = it->real_world;

    int i, count = it->count;
    for (i = 0; i < count; i ++) {
        ecs_entity_t r = it->entities[i];
        flecs_assert_relation_unused(world, r, EcsSymmetric);

        /* Create observer that adds the reverse relationship when R(X, Y) is
         * added, or remove the reverse relationship when R(X, Y) is removed. */
        ecs_observer(world, {
            .entity = ecs_entity(world, {.add = {ecs_childof(r)}}),
            .filter.terms[0] = { .id = ecs_pair(r, EcsWildcard) },
            .callback = flecs_on_symmetric_add_remove,
            .events = {EcsOnAdd, EcsOnRemove}
        });
    } 
}

static
void flecs_on_component(ecs_iter_t *it) {
    ecs_world_t *world = it->world;
    EcsComponent *c = ecs_field(it, EcsComponent, 1);

    int i, count = it->count;
    for (i = 0; i < count; i ++) {
        ecs_entity_t e = it->entities[i];

        uint32_t component_id = (uint32_t)e; /* Strip generation */
        ecs_assert(component_id < ECS_MAX_COMPONENT_ID, ECS_OUT_OF_RANGE,
            "component id must be smaller than %u", ECS_MAX_COMPONENT_ID);
        (void)component_id;

        if (it->event == EcsOnSet) {
            if (flecs_type_info_init_id(
                world, e, c[i].size, c[i].alignment, NULL))
            {
                flecs_assert_relation_unused(world, e, ecs_id(EcsComponent));
            }
        } else if (it->event == EcsOnRemove) {
            flecs_type_info_free(world, e);
        }
    }
}

static
void flecs_ensure_module_tag(ecs_iter_t *it) {
    ecs_world_t *world = it->world;

    int i, count = it->count;
    for (i = 0; i < count; i ++) {
        ecs_entity_t e = it->entities[i];
        ecs_entity_t parent = ecs_get_target(world, e, EcsChildOf, 0);
        if (parent) {
            ecs_add_id(world, parent, EcsModule);
        }
    }
}

/* -- Iterable mixins -- */

static
void flecs_on_event_iterable_init(
    const ecs_world_t *world,
    const ecs_poly_t *poly, /* Observable */
    ecs_iter_t *it,
    ecs_term_t *filter)
{
    ecs_iter_poly(world, poly, it, filter);
    it->event_id = filter->id;
}

/* -- Bootstrapping -- */

#define flecs_bootstrap_builtin_t(world, table, name)\
    flecs_bootstrap_builtin(world, table, ecs_id(name), #name, sizeof(name),\
        ECS_ALIGNOF(name))

static
void flecs_bootstrap_builtin(
    ecs_world_t *world,
    ecs_table_t *table,
    ecs_entity_t entity,
    const char *symbol,
    ecs_size_t size,
    ecs_size_t alignment)
{
    ecs_assert(table != NULL, ECS_INTERNAL_ERROR, NULL);

    ecs_vec_t *columns = table->data.columns;
    ecs_assert(columns != NULL, ECS_INTERNAL_ERROR, NULL);

    ecs_record_t *record = flecs_entities_ensure(world, entity);
    record->table = table;

    int32_t index = flecs_table_append(world, table, entity, record, false, false);
    record->row = ECS_ROW_TO_RECORD(index, 0);

    EcsComponent *component = ecs_vec_first(&columns[0]);
    component[index].size = size;
    component[index].alignment = alignment;

    const char *name = &symbol[3]; /* Strip 'Ecs' */
    ecs_size_t symbol_length = ecs_os_strlen(symbol);
    ecs_size_t name_length = symbol_length - 3;

    EcsIdentifier *name_col = ecs_vec_first(&columns[1]);
    uint64_t name_hash = flecs_hash(name, name_length);
    name_col[index].value = ecs_os_strdup(name);
    name_col[index].length = name_length;
    name_col[index].hash = name_hash;
    name_col[index].index_hash = 0;
    name_col[index].index = table->_->name_index;
    flecs_name_index_ensure(
        table->_->name_index, entity, name, name_length, name_hash);

    EcsIdentifier *symbol_col = ecs_vec_first(&columns[2]);
    symbol_col[index].value = ecs_os_strdup(symbol);
    symbol_col[index].length = symbol_length;
    symbol_col[index].hash = flecs_hash(symbol, symbol_length);    
    symbol_col[index].index_hash = 0;
    symbol_col[index].index = NULL;
}

/** Initialize component table. This table is manually constructed to bootstrap
 * flecs. After this function has been called, the builtin components can be
 * created. 
 * The reason this table is constructed manually is because it requires the size
 * and alignment of the EcsComponent and EcsIdentifier components, which haven't
 * been created yet */
static
ecs_table_t* flecs_bootstrap_component_table(
    ecs_world_t *world)
{
    /* Before creating table, manually set flags for ChildOf/Identifier, as this
     * can no longer be done after they are in use. */
    ecs_id_record_t *idr = flecs_id_record_ensure(world, EcsChildOf);
    idr->flags |= EcsIdOnDeleteObjectDelete | EcsIdDontInherit |
        EcsIdTraversable | EcsIdTag;

    /* Initialize id records cached on world */
    world->idr_childof_wildcard = flecs_id_record_ensure(world, 
        ecs_pair(EcsChildOf, EcsWildcard));
    world->idr_childof_wildcard->flags |= EcsIdOnDeleteObjectDelete | 
        EcsIdDontInherit | EcsIdTraversable | EcsIdTag | EcsIdExclusive;
    idr = flecs_id_record_ensure(world, ecs_pair_t(EcsIdentifier, EcsWildcard));
    idr->flags |= EcsIdDontInherit;
    world->idr_identifier_name = 
        flecs_id_record_ensure(world, ecs_pair_t(EcsIdentifier, EcsName));
    world->idr_childof_0 = flecs_id_record_ensure(world, 
        ecs_pair(EcsChildOf, 0));

    ecs_id_t ids[] = {
        ecs_id(EcsComponent), 
        EcsFinal,
        ecs_pair_t(EcsIdentifier, EcsName),
        ecs_pair_t(EcsIdentifier, EcsSymbol),
        ecs_pair(EcsChildOf, EcsFlecsCore),
        ecs_pair(EcsOnDelete, EcsPanic)
    };

    ecs_type_t array = {
        .array = ids,
        .count = 6
    };

    ecs_table_t *result = flecs_table_find_or_create(world, &array);
    ecs_data_t *data = &result->data;

    /* Preallocate enough memory for initial components */
    ecs_allocator_t *a = &world->allocator;
    ecs_vec_init_t(a, &data->entities, ecs_entity_t, EcsFirstUserComponentId);
    ecs_vec_init_t(a, &data->records, ecs_record_t*, EcsFirstUserComponentId);
    ecs_vec_init_t(a, &data->columns[0], EcsComponent, EcsFirstUserComponentId);
    ecs_vec_init_t(a, &data->columns[1], EcsIdentifier, EcsFirstUserComponentId);
    ecs_vec_init_t(a, &data->columns[2], EcsIdentifier, EcsFirstUserComponentId);
    
    return result;
}

static
void flecs_bootstrap_entity(
    ecs_world_t *world,
    ecs_entity_t id,
    const char *name,
    ecs_entity_t parent)
{
    char symbol[256];
    ecs_os_strcpy(symbol, "flecs.core.");
    ecs_os_strcat(symbol, name);
    
    ecs_ensure(world, id);
    ecs_add_pair(world, id, EcsChildOf, parent);
    ecs_set_name(world, id, name);
    ecs_set_symbol(world, id, symbol);

    ecs_assert(ecs_get_name(world, id) != NULL, ECS_INTERNAL_ERROR, NULL);

    if (!parent || parent == EcsFlecsCore) {
        ecs_assert(ecs_lookup_fullpath(world, name) == id, 
            ECS_INTERNAL_ERROR, NULL);
    }
}

void flecs_bootstrap(
    ecs_world_t *world)
{
    ecs_log_push();

    ecs_set_name_prefix(world, "Ecs");

    /* Ensure builtin ids are alive */
    ecs_ensure(world, ecs_id(EcsComponent));
    ecs_ensure(world, EcsFinal);
    ecs_ensure(world, ecs_id(EcsIdentifier));
    ecs_ensure(world, EcsName);
    ecs_ensure(world, EcsSymbol);
    ecs_ensure(world, EcsAlias);
    ecs_ensure(world, EcsChildOf);
    ecs_ensure(world, EcsFlecs);
    ecs_ensure(world, EcsFlecsCore);
    ecs_ensure(world, EcsOnAdd);
    ecs_ensure(world, EcsOnRemove);
    ecs_ensure(world, EcsOnSet);
    ecs_ensure(world, EcsUnSet);
    ecs_ensure(world, EcsOnDelete);
    ecs_ensure(world, EcsPanic);
    ecs_ensure(world, EcsFlag);
    ecs_ensure(world, EcsIsA);
    ecs_ensure(world, EcsWildcard);
    ecs_ensure(world, EcsAny);
    ecs_ensure(world, EcsTag);

    /* Register type information for builtin components */
    flecs_type_info_init(world, EcsComponent, { 
        .ctor = ecs_default_ctor,
        .on_set = flecs_on_component,
        .on_remove = flecs_on_component
    });

    flecs_type_info_init(world, EcsIdentifier, {
        .ctor = ecs_default_ctor,
        .dtor = ecs_dtor(EcsIdentifier),
        .copy = ecs_copy(EcsIdentifier),
        .move = ecs_move(EcsIdentifier),
        .on_set = ecs_on_set(EcsIdentifier),
        .on_remove = ecs_on_set(EcsIdentifier)
    });

    flecs_type_info_init(world, EcsPoly, {
        .ctor = ecs_default_ctor,
        .copy = ecs_copy(EcsPoly),
        .move = ecs_move(EcsPoly),
        .dtor = ecs_dtor(EcsPoly)
    });

    flecs_type_info_init(world, EcsIterable, { 0 });
    flecs_type_info_init(world, EcsTarget, { 0 });

    /* Create and cache often used id records on world */
    flecs_init_id_records(world);

    /* Create table for builtin components. This table temporarily stores the 
     * entities associated with builtin components, until they get moved to 
     * other tables once properties are added (see below) */
    ecs_table_t *table = flecs_bootstrap_component_table(world);
    assert(table != NULL);

    /* Bootstrap builtin components */
    flecs_bootstrap_builtin_t(world, table, EcsIdentifier);
    flecs_bootstrap_builtin_t(world, table, EcsComponent);
    flecs_bootstrap_builtin_t(world, table, EcsIterable);
    flecs_bootstrap_builtin_t(world, table, EcsPoly);
    flecs_bootstrap_builtin_t(world, table, EcsTarget);

    /* Initialize default entity id range */
    world->info.last_component_id = EcsFirstUserComponentId;
    flecs_entities_max_id(world) = EcsFirstUserEntityId;
    world->info.min_id = 0;
    world->info.max_id = 0;

    /* Make EcsOnAdd, EcsOnSet events iterable to enable .yield_existing */
    ecs_set(world, EcsOnAdd, EcsIterable, { .init = flecs_on_event_iterable_init });
    ecs_set(world, EcsOnSet, EcsIterable, { .init = flecs_on_event_iterable_init });
    
    /* Register observer for tag property before adding EcsTag */
    ecs_observer(world, {
        .entity = ecs_entity(world, {.add = { ecs_childof(EcsFlecsInternals)}}),
        .filter.terms[0] = { .id = EcsTag, .src.flags = EcsSelf },
        .events = {EcsOnAdd, EcsOnRemove},
        .callback = flecs_register_tag,
        .yield_existing = true
    });

    /* Populate core module */
    ecs_set_scope(world, EcsFlecsCore);

    flecs_bootstrap_tag(world, EcsName);
    flecs_bootstrap_tag(world, EcsSymbol);
    flecs_bootstrap_tag(world, EcsAlias);

    flecs_bootstrap_tag(world, EcsQuery);
    flecs_bootstrap_tag(world, EcsObserver);

    flecs_bootstrap_tag(world, EcsModule);
    flecs_bootstrap_tag(world, EcsPrivate);
    flecs_bootstrap_tag(world, EcsPrefab);
    flecs_bootstrap_tag(world, EcsSlotOf);
    flecs_bootstrap_tag(world, EcsDisabled);
    flecs_bootstrap_tag(world, EcsEmpty);

    /* Initialize builtin modules */
    ecs_set_name(world, EcsFlecs, "flecs");
    ecs_add_id(world, EcsFlecs, EcsModule);
    ecs_add_pair(world, EcsFlecs, EcsOnDelete, EcsPanic);

    ecs_add_pair(world, EcsFlecsCore, EcsChildOf, EcsFlecs);
    ecs_set_name(world, EcsFlecsCore, "core");
    ecs_add_id(world, EcsFlecsCore, EcsModule);

    ecs_add_pair(world, EcsFlecsInternals, EcsChildOf, EcsFlecsCore);
    ecs_set_name(world, EcsFlecsInternals, "internals");
    ecs_add_id(world, EcsFlecsInternals, EcsModule);

    /* Self check */
    ecs_record_t *r = flecs_entities_get(world, EcsFlecs);
    ecs_assert(r != NULL, ECS_INTERNAL_ERROR, NULL);
    ecs_assert(r->table != NULL, ECS_INTERNAL_ERROR, NULL);
    ecs_assert(r->row & EcsEntityIsTraversable, ECS_INTERNAL_ERROR, NULL);
    (void)r;

    /* Initialize builtin entities */
    flecs_bootstrap_entity(world, EcsWorld, "World", EcsFlecsCore);
    flecs_bootstrap_entity(world, EcsWildcard, "*", EcsFlecsCore);
    flecs_bootstrap_entity(world, EcsAny, "_", EcsFlecsCore);
    flecs_bootstrap_entity(world, EcsThis, "this", EcsFlecsCore);
    flecs_bootstrap_entity(world, EcsVariable, "$", EcsFlecsCore);
    flecs_bootstrap_entity(world, EcsFlag, "Flag", EcsFlecsCore);

    /* Component/relationship properties */
    flecs_bootstrap_tag(world, EcsTransitive);
    flecs_bootstrap_tag(world, EcsReflexive);
    flecs_bootstrap_tag(world, EcsSymmetric);
    flecs_bootstrap_tag(world, EcsFinal);
    flecs_bootstrap_tag(world, EcsDontInherit);
    flecs_bootstrap_tag(world, EcsAlwaysOverride);
    flecs_bootstrap_tag(world, EcsTag);
    flecs_bootstrap_tag(world, EcsUnion);
    flecs_bootstrap_tag(world, EcsExclusive);
    flecs_bootstrap_tag(world, EcsAcyclic);
    flecs_bootstrap_tag(world, EcsTraversable);
    flecs_bootstrap_tag(world, EcsWith);
    flecs_bootstrap_tag(world, EcsOneOf);

    flecs_bootstrap_tag(world, EcsOnDelete);
    flecs_bootstrap_tag(world, EcsOnDeleteTarget);
    flecs_bootstrap_tag(world, EcsRemove);
    flecs_bootstrap_tag(world, EcsDelete);
    flecs_bootstrap_tag(world, EcsPanic);

    flecs_bootstrap_tag(world, EcsFlatten);
    flecs_bootstrap_tag(world, EcsDefaultChildComponent);

    /* Builtin predicates */
    flecs_bootstrap_tag(world, EcsPredEq);
    flecs_bootstrap_tag(world, EcsPredMatch);
    flecs_bootstrap_tag(world, EcsPredLookup);
    flecs_bootstrap_tag(world, EcsScopeOpen);
    flecs_bootstrap_tag(world, EcsScopeClose);

    /* Builtin relationships */
    flecs_bootstrap_tag(world, EcsIsA);
    flecs_bootstrap_tag(world, EcsChildOf);
    flecs_bootstrap_tag(world, EcsDependsOn);

    /* Builtin events */
    flecs_bootstrap_entity(world, EcsOnAdd, "OnAdd", EcsFlecsCore);
    flecs_bootstrap_entity(world, EcsOnRemove, "OnRemove", EcsFlecsCore);
    flecs_bootstrap_entity(world, EcsOnSet, "OnSet", EcsFlecsCore);
    flecs_bootstrap_entity(world, EcsUnSet, "UnSet", EcsFlecsCore);
    flecs_bootstrap_entity(world, EcsMonitor, "EcsMonitor", EcsFlecsCore);
    flecs_bootstrap_entity(world, EcsOnTableCreate, "OnTableCreate", EcsFlecsCore);
    flecs_bootstrap_entity(world, EcsOnTableDelete, "OnTableDelete", EcsFlecsCore);
    flecs_bootstrap_entity(world, EcsOnTableEmpty, "OnTableEmpty", EcsFlecsCore);
    flecs_bootstrap_entity(world, EcsOnTableFill, "OnTableFilled", EcsFlecsCore);

    /* Tag relationships (relationships that should never have data) */
    ecs_add_id(world, EcsIsA, EcsTag);
    ecs_add_id(world, EcsChildOf, EcsTag);
    ecs_add_id(world, EcsSlotOf, EcsTag);
    ecs_add_id(world, EcsDependsOn, EcsTag);
    ecs_add_id(world, EcsFlatten, EcsTag);
    ecs_add_id(world, EcsDefaultChildComponent, EcsTag);
    ecs_add_id(world, EcsUnion, EcsTag);
    ecs_add_id(world, EcsFlag, EcsTag);
    ecs_add_id(world, EcsWith, EcsTag);

    /* Exclusive properties */
    ecs_add_id(world, EcsChildOf, EcsExclusive);
    ecs_add_id(world, EcsOnDelete, EcsExclusive);
    ecs_add_id(world, EcsOnDeleteTarget, EcsExclusive);
    ecs_add_id(world, EcsDefaultChildComponent, EcsExclusive);

    /* Sync properties of ChildOf and Identifier with bootstrapped flags */
    ecs_add_pair(world, EcsChildOf, EcsOnDeleteTarget, EcsDelete);
    ecs_add_id(world, EcsChildOf, EcsAcyclic);
    ecs_add_id(world, EcsChildOf, EcsTraversable);
    ecs_add_id(world, EcsChildOf, EcsDontInherit);
    ecs_add_id(world, ecs_id(EcsIdentifier), EcsDontInherit);

    /* Create triggers in internals scope */
    ecs_set_scope(world, EcsFlecsInternals);

    /* Term used to also match prefabs */
    ecs_term_t match_prefab = { 
        .id = EcsPrefab, 
        .oper = EcsOptional,
        .src.flags = EcsSelf 
    };

    /* Register observers for components/relationship properties. Most observers
     * set flags on an id record when a property is added to a component, which
     * allows for quick property testing in various operations. */
    ecs_observer(world, {
        .filter.terms = {{ .id = EcsFinal, .src.flags = EcsSelf }, match_prefab },
        .events = {EcsOnAdd},
        .callback = flecs_register_final
    });

    ecs_observer(world, {
        .filter.terms = {
            { .id = ecs_pair(EcsOnDelete, EcsWildcard), .src.flags = EcsSelf },
            match_prefab
        },
        .events = {EcsOnAdd, EcsOnRemove},
        .callback = flecs_register_on_delete
    });

    ecs_observer(world, {
        .filter.terms = {
            { .id = ecs_pair(EcsOnDeleteTarget, EcsWildcard), .src.flags = EcsSelf },
            match_prefab
        },
        .events = {EcsOnAdd, EcsOnRemove},
        .callback = flecs_register_on_delete_object
    });

    ecs_observer(world, {
        .filter.terms = {
            { .id = EcsTraversable, .src.flags = EcsSelf },
            match_prefab
        },
        .events = {EcsOnAdd, EcsOnRemove},
        .callback = flecs_register_traversable
    });

    ecs_observer(world, {
        .filter.terms = {{ .id = EcsExclusive, .src.flags = EcsSelf  }, match_prefab },
        .events = {EcsOnAdd, EcsOnRemove},
        .callback = flecs_register_exclusive
    });

    ecs_observer(world, {
        .filter.terms = {{ .id = EcsSymmetric, .src.flags = EcsSelf  }, match_prefab },
        .events = {EcsOnAdd},
        .callback = flecs_register_symmetric
    });

    ecs_observer(world, {
        .filter.terms = {{ .id = EcsDontInherit, .src.flags = EcsSelf }, match_prefab },
        .events = {EcsOnAdd},
        .callback = flecs_register_dont_inherit
    });

    ecs_observer(world, {
        .filter.terms = {{ .id = EcsAlwaysOverride, .src.flags = EcsSelf } },
        .events = {EcsOnAdd},
        .callback = flecs_register_always_override
    });

    ecs_observer(world, {
        .filter.terms = {
            { .id = ecs_pair(EcsWith, EcsWildcard), .src.flags = EcsSelf },
            match_prefab
        },
        .events = {EcsOnAdd},
        .callback = flecs_register_with
    });

    ecs_observer(world, {
        .filter.terms = {{ .id = EcsUnion, .src.flags = EcsSelf }, match_prefab },
        .events = {EcsOnAdd},
        .callback = flecs_register_union
    });

    /* Entities used as slot are marked as exclusive to ensure a slot can always
     * only point to a single entity. */
    ecs_observer(world, {
        .filter.terms = {
            { .id = ecs_pair(EcsSlotOf, EcsWildcard), .src.flags = EcsSelf },
            match_prefab
        },
        .events = {EcsOnAdd},
        .callback = flecs_register_slot_of
    });

    /* Define observer to make sure that adding a module to a child entity also
     * adds it to the parent. */
    ecs_observer(world, {
        .filter.terms = {{ .id = EcsModule, .src.flags = EcsSelf }, match_prefab},
        .events = {EcsOnAdd},
        .callback = flecs_ensure_module_tag
    });

    /* Set scope back to flecs core */
    ecs_set_scope(world, EcsFlecsCore);

    /* Traversable relationships are always acyclic */
    ecs_add_pair(world, EcsTraversable, EcsWith, EcsAcyclic);

    /* Transitive relationships are always Traversable */
    ecs_add_pair(world, EcsTransitive, EcsWith, EcsTraversable);

    /* DontInherit components */
    ecs_add_id(world, EcsPrefab, EcsDontInherit);

    /* Acyclic/Traversable components */
    ecs_add_id(world, EcsIsA, EcsTraversable);
    ecs_add_id(world, EcsDependsOn, EcsTraversable);
    ecs_add_id(world, EcsWith, EcsAcyclic);

    /* Transitive relationships */
    ecs_add_id(world, EcsIsA, EcsTransitive);
    ecs_add_id(world, EcsIsA, EcsReflexive);

    /* Exclusive properties */
    ecs_add_id(world, EcsSlotOf, EcsExclusive);
    ecs_add_id(world, EcsOneOf, EcsExclusive);
    ecs_add_id(world, EcsFlatten, EcsExclusive);
    
    /* Run bootstrap functions for other parts of the code */
    flecs_bootstrap_hierarchy(world);

    ecs_set_scope(world, 0);

    ecs_set_name_prefix(world, NULL);

    ecs_log_pop();
}

/**
 * @file hierarchy.c
 * @brief API for entity paths and name lookups.
 */

#include <ctype.h>

#define ECS_NAME_BUFFER_LENGTH (64)

static
bool flecs_path_append(
    const ecs_world_t *world, 
    ecs_entity_t parent, 
    ecs_entity_t child, 
    const char *sep,
    const char *prefix,
    ecs_strbuf_t *buf)
{
    ecs_poly_assert(world, ecs_world_t);
    ecs_assert(sep[0] != 0, ECS_INVALID_PARAMETER, NULL);

    ecs_entity_t cur = 0;
    const char *name = NULL;
    ecs_size_t name_len = 0;

    if (child && ecs_is_alive(world, child)) {
        cur = ecs_get_target(world, child, EcsChildOf, 0);
        if (cur) {
            ecs_assert(cur != child, ECS_CYCLE_DETECTED, NULL);
            if (cur != parent && (cur != EcsFlecsCore || prefix != NULL)) {
                flecs_path_append(world, parent, cur, sep, prefix, buf);
                if (!sep[1]) {
                    ecs_strbuf_appendch(buf, sep[0]);
                } else {
                    ecs_strbuf_appendstr(buf, sep);
                }
            }
        } else if (prefix && prefix[0]) {
            if (!prefix[1]) {
                ecs_strbuf_appendch(buf, prefix[0]);
            } else {
                ecs_strbuf_appendstr(buf, prefix);
            }
        }

        const EcsIdentifier *id = ecs_get_pair(
            world, child, EcsIdentifier, EcsName);
        if (id) {
            name = id->value;
            name_len = id->length;
        }      
    }

    if (name) {
        ecs_strbuf_appendstrn(buf, name, name_len);
    } else {
        ecs_strbuf_appendint(buf, flecs_uto(int64_t, (uint32_t)child));
    }

    return cur != 0;
}

bool flecs_name_is_id(
    const char *name)
{
    ecs_assert(name != NULL, ECS_INTERNAL_ERROR, NULL);
    
    if (!isdigit(name[0])) {
        return false;
    }

    ecs_size_t i, length = ecs_os_strlen(name);
    for (i = 1; i < length; i ++) {
        char ch = name[i];

        if (!isdigit(ch)) {
            break;
        }
    }

    return i >= length;
}

ecs_entity_t flecs_name_to_id(
    const ecs_world_t *world,
    const char *name)
{
    int64_t result = atoll(name);
    ecs_assert(result >= 0, ECS_INTERNAL_ERROR, NULL);
    ecs_entity_t alive = ecs_get_alive(world, (ecs_entity_t)result);
    if (alive) {
        return alive;
    } else {
        if ((uint32_t)result == (uint64_t)result) {
            return (ecs_entity_t)result;
        } else {
            return 0;
        }
    }
}

static
ecs_entity_t flecs_get_builtin(
    const char *name)
{
    if (name[0] == '.' && name[1] == '\0') {
        return EcsThis;
    } else if (name[0] == '*' && name[1] == '\0') {
        return EcsWildcard;
    } else if (name[0] == '_' && name[1] == '\0') {
        return EcsAny;
    } else if (name[0] == '$' && name[1] == '\0') {
        return EcsVariable;
    }

    return 0;
}

static
bool flecs_is_sep(
    const char **ptr,
    const char *sep)
{
    ecs_size_t len = ecs_os_strlen(sep);

    if (!ecs_os_strncmp(*ptr, sep, len)) {
        *ptr += len;
        return true;
    } else {
        return false;
    }
}

static
const char* flecs_path_elem(
    const char *path,
    const char *sep,
    int32_t *len)
{
    const char *ptr;
    char ch;
    int32_t template_nesting = 0;
    int32_t count = 0;

    for (ptr = path; (ch = *ptr); ptr ++) {
        if (ch == '<') {
            template_nesting ++;
        } else if (ch == '>') {
            template_nesting --;
        }

        ecs_check(template_nesting >= 0, ECS_INVALID_PARAMETER, path);

        if (!template_nesting && flecs_is_sep(&ptr, sep)) {
            break;
        }

        count ++;
    }

    if (len) {
        *len = count;
    }

    if (count) {
        return ptr;
    } else {
        return NULL;
    }
error:
    return NULL;
}

static
bool flecs_is_root_path(
    const char *path,
    const char *prefix)
{
    if (prefix) {
        return !ecs_os_strncmp(path, prefix, ecs_os_strlen(prefix));
    } else {
        return false;
    }
}

static
ecs_entity_t flecs_get_parent_from_path(
    const ecs_world_t *world,
    ecs_entity_t parent,
    const char **path_ptr,
    const char *prefix,
    bool new_entity)
{
    bool start_from_root = false;
    const char *path = *path_ptr;

    if (flecs_is_root_path(path, prefix)) {
        path += ecs_os_strlen(prefix);
        parent = 0;
        start_from_root = true;
    }

    if (!start_from_root && !parent && new_entity) {
        parent = ecs_get_scope(world);
    }

    *path_ptr = path;

    return parent;
}

static
void flecs_on_set_symbol(ecs_iter_t *it) {
    EcsIdentifier *n = ecs_field(it, EcsIdentifier, 1);
    ecs_world_t *world = it->world;

    int i;
    for (i = 0; i < it->count; i ++) {
        ecs_entity_t e = it->entities[i];
        flecs_name_index_ensure(
            &world->symbols, e, n[i].value, n[i].length, n[i].hash);
    }
}

void flecs_bootstrap_hierarchy(ecs_world_t *world) {
    ecs_observer(world, {
        .entity = ecs_entity(world, {.add = {ecs_childof(EcsFlecsInternals)}}),
        .filter.terms[0] = {
            .id = ecs_pair(ecs_id(EcsIdentifier), EcsSymbol), 
            .src.flags = EcsSelf 
        },
        .callback = flecs_on_set_symbol,
        .events = {EcsOnSet},
        .yield_existing = true
    });
}


/* Public functions */

void ecs_get_path_w_sep_buf(
    const ecs_world_t *world,
    ecs_entity_t parent,
    ecs_entity_t child,
    const char *sep,
    const char *prefix,
    ecs_strbuf_t *buf)
{
    ecs_check(world != NULL, ECS_INVALID_PARAMETER, NULL);
    ecs_check(buf != NULL, ECS_INVALID_PARAMETER, NULL);

    world = ecs_get_world(world);

    if (child == EcsWildcard) {
        ecs_strbuf_appendch(buf, '*');
        return;
    }
    if (child == EcsAny) {
        ecs_strbuf_appendch(buf, '_');
        return;
    }

    if (!sep) {
        sep = ".";
    }

    if (!child || parent != child) {
        flecs_path_append(world, parent, child, sep, prefix, buf);
    } else {
        ecs_strbuf_appendstrn(buf, "", 0);
    }

error:
    return;
}

char* ecs_get_path_w_sep(
    const ecs_world_t *world,
    ecs_entity_t parent,
    ecs_entity_t child,
    const char *sep,
    const char *prefix)
{
    ecs_strbuf_t buf = ECS_STRBUF_INIT;
    ecs_get_path_w_sep_buf(world, parent, child, sep, prefix, &buf);
    return ecs_strbuf_get(&buf);
}

ecs_entity_t ecs_lookup_child(
    const ecs_world_t *world,
    ecs_entity_t parent,
    const char *name)
{
    ecs_check(world != NULL, ECS_INTERNAL_ERROR, NULL);
    world = ecs_get_world(world);

    if (flecs_name_is_id(name)) {
        ecs_entity_t result = flecs_name_to_id(world, name);
        if (result && ecs_is_alive(world, result)) {
            if (parent && !ecs_has_pair(world, result, EcsChildOf, parent)) {
                return 0;
            }
            return result;
        }
    }

    ecs_id_t pair = ecs_childof(parent);
    ecs_hashmap_t *index = flecs_id_name_index_get(world, pair);
    if (index) {
        return flecs_name_index_find(index, name, 0, 0);
    } else {
        return 0;
    }
error:
    return 0;
}

ecs_entity_t ecs_lookup(
    const ecs_world_t *world,
    const char *name)
{   
    if (!name) {
        return 0;
    }

    ecs_check(world != NULL, ECS_INTERNAL_ERROR, NULL);
    world = ecs_get_world(world);

    ecs_entity_t e = flecs_get_builtin(name);
    if (e) {
        return e;
    }

    if (flecs_name_is_id(name)) {
        return flecs_name_to_id(world, name);
    }

    e = flecs_name_index_find(&world->aliases, name, 0, 0);
    if (e) {
        return e;
    }    
    
    return ecs_lookup_child(world, 0, name);
error:
    return 0;
}

ecs_entity_t ecs_lookup_symbol(
    const ecs_world_t *world,
    const char *name,
    bool lookup_as_path)
{   
    if (!name) {
        return 0;
    }

    ecs_check(world != NULL, ECS_INTERNAL_ERROR, NULL);
    world = ecs_get_world(world);

    ecs_entity_t e = flecs_name_index_find(&world->symbols, name, 0, 0);
    if (e) {
        return e;
    }

    if (lookup_as_path) {
        return ecs_lookup_fullpath(world, name);
    }

error:
    return 0;
}

ecs_entity_t ecs_lookup_path_w_sep(
    const ecs_world_t *world,
    ecs_entity_t parent,
    const char *path,
    const char *sep,
    const char *prefix,
    bool recursive)
{
    if (!path) {
        return 0;
    }

    ecs_check(world != NULL, ECS_INTERNAL_ERROR, NULL);
    const ecs_world_t *stage = world;
    world = ecs_get_world(world);

    ecs_entity_t e = flecs_get_builtin(path);
    if (e) {
        return e;
    }

    e = flecs_name_index_find(&world->aliases, path, 0, 0);
    if (e) {
        return e;
    }

    char buff[ECS_NAME_BUFFER_LENGTH];
    const char *ptr, *ptr_start;
    char *elem = buff;
    int32_t len, size = ECS_NAME_BUFFER_LENGTH;
    ecs_entity_t cur;
    bool lookup_path_search = false;

    ecs_entity_t *lookup_path = ecs_get_lookup_path(stage);
    ecs_entity_t *lookup_path_cur = lookup_path;
    while (lookup_path_cur && *lookup_path_cur) {
        lookup_path_cur ++;
    }

    if (!sep) {
        sep = ".";
    }

    parent = flecs_get_parent_from_path(stage, parent, &path, prefix, true);

    if (!sep[0]) {
        return ecs_lookup_child(world, parent, path);
    }

retry:
    cur = parent;
    ptr_start = ptr = path;

    while ((ptr = flecs_path_elem(ptr, sep, &len))) {
        if (len < size) {
            ecs_os_memcpy(elem, ptr_start, len);
        } else {
            if (size == ECS_NAME_BUFFER_LENGTH) {
                elem = NULL;
            }

            elem = ecs_os_realloc(elem, len + 1);
            ecs_os_memcpy(elem, ptr_start, len);
            size = len + 1;
        }

        elem[len] = '\0';
        ptr_start = ptr;

        cur = ecs_lookup_child(world, cur, elem);
        if (!cur) {
            goto tail;
        }
    }

tail:
    if (!cur && recursive) {
        if (!lookup_path_search) {
            if (parent) {
                parent = ecs_get_target(world, parent, EcsChildOf, 0);
                goto retry;
            } else {
                lookup_path_search = true;
            }
        }

        if (lookup_path_search) {
            if (lookup_path_cur != lookup_path) {
                lookup_path_cur --;
                parent = lookup_path_cur[0];
                goto retry;
            }
        }
    }

    if (elem != buff) {
        ecs_os_free(elem);
    }

    return cur;
error:
    return 0;
}

ecs_entity_t ecs_set_scope(
    ecs_world_t *world,
    ecs_entity_t scope)
{
    ecs_check(world != NULL, ECS_INVALID_PARAMETER, NULL);
    ecs_stage_t *stage = flecs_stage_from_world(&world);

    ecs_entity_t cur = stage->scope;
    stage->scope = scope;

    return cur;
error:
    return 0;
}

ecs_entity_t ecs_get_scope(
    const ecs_world_t *world)
{
    ecs_check(world != NULL, ECS_INVALID_PARAMETER, NULL);
    const ecs_stage_t *stage = flecs_stage_from_readonly_world(world);
    return stage->scope;
error:
    return 0;
}

ecs_entity_t* ecs_set_lookup_path(
    ecs_world_t *world,
    const ecs_entity_t *lookup_path)
{
    ecs_check(world != NULL, ECS_INVALID_PARAMETER, NULL);
    ecs_stage_t *stage = flecs_stage_from_world(&world);

    ecs_entity_t *cur = stage->lookup_path;
    stage->lookup_path = (ecs_entity_t*)lookup_path;

    return cur;
error:
    return NULL;
}

ecs_entity_t* ecs_get_lookup_path(
    const ecs_world_t *world)
{
    ecs_check(world != NULL, ECS_INVALID_PARAMETER, NULL);
    const ecs_stage_t *stage = flecs_stage_from_readonly_world(world);
    return stage->lookup_path;
error:
    return NULL;
}

const char* ecs_set_name_prefix(
    ecs_world_t *world,
    const char *prefix)
{
    ecs_poly_assert(world, ecs_world_t);
    const char *old_prefix = world->info.name_prefix;
    world->info.name_prefix = prefix;
    return old_prefix;
}

static
void flecs_add_path(
    ecs_world_t *world,
    bool defer_suspend,
    ecs_entity_t parent,
    ecs_entity_t entity,
    const char *name)
{
    ecs_suspend_readonly_state_t srs;
    ecs_world_t *real_world = NULL;
    if (defer_suspend) {
        real_world = flecs_suspend_readonly(world, &srs);
        ecs_assert(real_world != NULL, ECS_INTERNAL_ERROR, NULL);
    }

    if (parent) {
        ecs_add_pair(world, entity, EcsChildOf, parent);
    }

    ecs_set_name(world, entity, name);

    if (defer_suspend) {
        flecs_resume_readonly(real_world, &srs);
        flecs_defer_path((ecs_stage_t*)world, parent, entity, name);
    }
}

ecs_entity_t ecs_add_path_w_sep(
    ecs_world_t *world,
    ecs_entity_t entity,
    ecs_entity_t parent,
    const char *path,
    const char *sep,
    const char *prefix)
{
    ecs_check(world != NULL, ECS_INVALID_PARAMETER, NULL);

    if (!sep) {
        sep = ".";
    }

    if (!path) {
        if (!entity) {
            entity = ecs_new_id(world);
        }

        if (parent) {
            ecs_add_pair(world, entity, EcsChildOf, entity);
        }

        return entity;
    }

    bool root_path = flecs_is_root_path(path, prefix);
    parent = flecs_get_parent_from_path(world, parent, &path, prefix, !entity);

    char buff[ECS_NAME_BUFFER_LENGTH];
    const char *ptr = path;
    const char *ptr_start = path;
    char *elem = buff;
    int32_t len, size = ECS_NAME_BUFFER_LENGTH;
    
    /* If we're in deferred/readonly mode suspend it, so that the name index is
     * immediately updated. Without this, we could create multiple entities for
     * the same name in a single command queue. */
    bool suspend_defer = ecs_poly_is(world, ecs_stage_t) && 
        (ecs_get_stage_count(world) <= 1);
    ecs_entity_t cur = parent;
    char *name = NULL;

    if (sep[0]) {
        while ((ptr = flecs_path_elem(ptr, sep, &len))) {
            if (len < size) {
                ecs_os_memcpy(elem, ptr_start, len);
            } else {
                if (size == ECS_NAME_BUFFER_LENGTH) {
                    elem = NULL;
                }

                elem = ecs_os_realloc(elem, len + 1);
                ecs_os_memcpy(elem, ptr_start, len);
                size = len + 1;          
            }

            elem[len] = '\0';
            ptr_start = ptr;

            ecs_entity_t e = ecs_lookup_child(world, cur, elem);
            if (!e) {
                if (name) {
                    ecs_os_free(name);
                }

                name = ecs_os_strdup(elem);

                /* If this is the last entity in the path, use the provided id */
                bool last_elem = false;
                if (!flecs_path_elem(ptr, sep, NULL)) {
                    e = entity;
                    last_elem = true;
                }

                if (!e) {
                    if (last_elem) {
                        ecs_entity_t prev = ecs_set_scope(world, 0);
                        e = ecs_new(world, 0);
                        ecs_set_scope(world, prev);
                    } else {
                        e = ecs_new_id(world);
                    }
                }

                if (!cur && last_elem && root_path) {
                    ecs_remove_pair(world, e, EcsChildOf, EcsWildcard);
                }

                flecs_add_path(world, suspend_defer, cur, e, name);
            }

            cur = e;
        }

        if (entity && (cur != entity)) {
            ecs_throw(ECS_ALREADY_DEFINED, name);
        }

        if (name) {
            ecs_os_free(name);
        }

        if (elem != buff) {
            ecs_os_free(elem);
        }
    } else {
        flecs_add_path(world, suspend_defer, parent, entity, path);
    }

    return cur;
error:
    return 0;
}

ecs_entity_t ecs_new_from_path_w_sep(
    ecs_world_t *world,
    ecs_entity_t parent,
    const char *path,
    const char *sep,
    const char *prefix)
{
    return ecs_add_path_w_sep(world, 0, parent, path, sep, prefix);
}

/**
 * @file id_record.c
 * @brief Index for looking up tables by (component) id.
 * 
 * An id record stores the administration for an in use (component) id, that is
 * an id that has been used in tables.
 * 
 * An id record contains a table cache, which stores the list of tables that
 * have the id. Each entry in the cache (a table record) stores the first 
 * occurrence of the id in the table and the number of occurrences of the id in
 * the table (in the case of wildcard ids).
 * 
 * Id records are used in lots of scenarios, like uncached queries, or for 
 * getting a component array/component for an entity.
 */


static
ecs_id_record_elem_t* flecs_id_record_elem(
    ecs_id_record_t *head,
    ecs_id_record_elem_t *list,
    ecs_id_record_t *idr)
{
    return ECS_OFFSET(idr, (uintptr_t)list - (uintptr_t)head);
}

static
void flecs_id_record_elem_insert(
    ecs_id_record_t *head,
    ecs_id_record_t *idr,
    ecs_id_record_elem_t *elem)
{
    ecs_id_record_elem_t *head_elem = flecs_id_record_elem(idr, elem, head);
    ecs_id_record_t *cur = head_elem->next;
    elem->next = cur;
    elem->prev = head;
    if (cur) {
        ecs_id_record_elem_t *cur_elem = flecs_id_record_elem(idr, elem, cur);
        cur_elem->prev = idr;
    }
    head_elem->next = idr;
}

static
void flecs_id_record_elem_remove(
    ecs_id_record_t *idr,
    ecs_id_record_elem_t *elem)
{
    ecs_id_record_t *prev = elem->prev;
    ecs_id_record_t *next = elem->next;
    ecs_assert(prev != NULL, ECS_INTERNAL_ERROR, NULL);

    ecs_id_record_elem_t *prev_elem = flecs_id_record_elem(idr, elem, prev);
    prev_elem->next = next;
    if (next) {
        ecs_id_record_elem_t *next_elem = flecs_id_record_elem(idr, elem, next);
        next_elem->prev = prev;
    }
}

static
void flecs_insert_id_elem(
    ecs_world_t *world,
    ecs_id_record_t *idr,
    ecs_id_t wildcard,
    ecs_id_record_t *widr)
{
    ecs_assert(ecs_id_is_wildcard(wildcard), ECS_INTERNAL_ERROR, NULL);
    if (!widr) {
        widr = flecs_id_record_ensure(world, wildcard);
    }
    ecs_assert(widr != NULL, ECS_INTERNAL_ERROR, NULL);

    if (ECS_PAIR_SECOND(wildcard) == EcsWildcard) {
        ecs_assert(ECS_PAIR_FIRST(wildcard) != EcsWildcard, 
            ECS_INTERNAL_ERROR, NULL);
        flecs_id_record_elem_insert(widr, idr, &idr->first);
    } else {
        ecs_assert(ECS_PAIR_FIRST(wildcard) == EcsWildcard, 
            ECS_INTERNAL_ERROR, NULL);
        flecs_id_record_elem_insert(widr, idr, &idr->second);

        if (idr->flags & EcsIdTraversable) {
            flecs_id_record_elem_insert(widr, idr, &idr->trav);
        }
    }
}

static
void flecs_remove_id_elem(
    ecs_id_record_t *idr,
    ecs_id_t wildcard)
{
    ecs_assert(ecs_id_is_wildcard(wildcard), ECS_INTERNAL_ERROR, NULL);

    if (ECS_PAIR_SECOND(wildcard) == EcsWildcard) {
        ecs_assert(ECS_PAIR_FIRST(wildcard) != EcsWildcard, 
            ECS_INTERNAL_ERROR, NULL);
        flecs_id_record_elem_remove(idr, &idr->first);
    } else {
        ecs_assert(ECS_PAIR_FIRST(wildcard) == EcsWildcard, 
            ECS_INTERNAL_ERROR, NULL);
        flecs_id_record_elem_remove(idr, &idr->second);

        if (idr->flags & EcsIdTraversable) {
            flecs_id_record_elem_remove(idr, &idr->trav);
        }
    }
}

static
ecs_id_t flecs_id_record_hash(
    ecs_id_t id)
{
    id = ecs_strip_generation(id);
    if (ECS_IS_PAIR(id)) {
        ecs_entity_t r = ECS_PAIR_FIRST(id);
        ecs_entity_t o = ECS_PAIR_SECOND(id);
        if (r == EcsAny) {
            r = EcsWildcard;
        }
        if (o == EcsAny) {
            o = EcsWildcard;
        }
        id = ecs_pair(r, o);
    }
    return id;
}

static
ecs_id_record_t* flecs_id_record_new(
    ecs_world_t *world,
    ecs_id_t id)
{
    ecs_id_record_t *idr, *idr_t = NULL;
    ecs_id_t hash = flecs_id_record_hash(id);
    if (hash >= FLECS_HI_ID_RECORD_ID) {
        idr = flecs_bcalloc(&world->allocators.id_record);
        ecs_map_insert_ptr(&world->id_index_hi, hash, idr);
    } else {
        idr = &world->id_index_lo[hash];
        ecs_os_zeromem(idr);
    }

    ecs_table_cache_init(world, &idr->cache);

    idr->id = id;
    idr->refcount = 1;
    idr->reachable.current = -1;

    bool is_wildcard = ecs_id_is_wildcard(id);
    bool is_pair = ECS_IS_PAIR(id);

    ecs_entity_t rel = 0, tgt = 0, role = id & ECS_ID_FLAGS_MASK;
    if (is_pair) {
        // rel = ecs_pair_first(world, id);
        rel = ECS_PAIR_FIRST(id);
        ecs_assert(rel != 0, ECS_INTERNAL_ERROR, NULL);

        /* Relationship object can be 0, as tables without a ChildOf 
         * relationship are added to the (ChildOf, 0) id record */
        tgt = ECS_PAIR_SECOND(id);

#ifdef FLECS_DEBUG
        /* Check constraints */
        if (tgt) {
            tgt = ecs_get_alive(world, tgt);
            ecs_assert(tgt != 0, ECS_INTERNAL_ERROR, NULL);
        }
        if (tgt && !ecs_id_is_wildcard(tgt)) {
            /* Check if target of relationship satisfies OneOf property */
            ecs_entity_t oneof = flecs_get_oneof(world, rel);
            ecs_check( !oneof || ecs_has_pair(world, tgt, EcsChildOf, oneof),
                ECS_CONSTRAINT_VIOLATED, NULL);
            (void)oneof;

            /* Check if we're not trying to inherit from a final target */
            if (rel == EcsIsA) {
                bool is_final = ecs_has_id(world, tgt, EcsFinal);
                ecs_check(!is_final, ECS_CONSTRAINT_VIOLATED, 
                    "cannot inherit from final entity");
                (void)is_final;
            }
        }
#endif

        if (!is_wildcard && (rel != EcsFlag)) {
            /* Inherit flags from (relationship, *) record */
            ecs_id_record_t *idr_r = flecs_id_record_ensure(
                world, ecs_pair(rel, EcsWildcard));
            idr->parent = idr_r;
            idr->flags = idr_r->flags;

            /* If pair is not a wildcard, append it to wildcard lists. These 
             * allow for quickly enumerating all relationships for an object, 
             * or all objecs for a relationship. */
            flecs_insert_id_elem(world, idr, ecs_pair(rel, EcsWildcard), idr_r);

            idr_t = flecs_id_record_ensure(world, ecs_pair(EcsWildcard, tgt));
            flecs_insert_id_elem(world, idr, ecs_pair(EcsWildcard, tgt), idr_t);

            if (rel == EcsUnion) {
                idr->flags |= EcsIdUnion;
            }
        }
    } else {
        rel = id & ECS_COMPONENT_MASK;
        ecs_assert(rel != 0, ECS_INTERNAL_ERROR, NULL);
    }

    /* Initialize type info if id is not a tag */
    if (!is_wildcard && (!role || is_pair)) {
        if (!(idr->flags & EcsIdTag)) {
            const ecs_type_info_t *ti = flecs_type_info_get(world, rel);
            if (!ti && tgt) {
                ti = flecs_type_info_get(world, tgt);
            }
            idr->type_info = ti;
        }
    }

    /* Mark entities that are used as component/pair ids. When a tracked
     * entity is deleted, cleanup policies are applied so that the store
     * won't contain any tables with deleted ids. */

    /* Flag for OnDelete policies */
    flecs_add_flag(world, rel, EcsEntityIsId);
    if (tgt) {
        /* Flag for OnDeleteTarget policies */
        ecs_record_t *tgt_r = flecs_entities_get_any(world, tgt);
        ecs_assert(tgt_r != NULL, ECS_INTERNAL_ERROR, NULL);
        flecs_record_add_flag(tgt_r, EcsEntityIsTarget);
        if (idr->flags & EcsIdTraversable) {
            /* Flag used to determine if object should be traversed when
             * propagating events or with super/subset queries */
            flecs_record_add_flag(tgt_r, EcsEntityIsTraversable);

            /* Add reference to (*, tgt) id record to entity record */
            tgt_r->idr = idr_t;
        }
    }

    ecs_observable_t *o = &world->observable;
    idr->flags |= flecs_observers_exist(o, id, EcsOnAdd) * EcsIdHasOnAdd;
    idr->flags |= flecs_observers_exist(o, id, EcsOnRemove) * EcsIdHasOnRemove;
    idr->flags |= flecs_observers_exist(o, id, EcsOnSet) * EcsIdHasOnSet;
    idr->flags |= flecs_observers_exist(o, id, EcsUnSet) * EcsIdHasUnSet;
    idr->flags |= flecs_observers_exist(o, id, EcsOnTableFill) * EcsIdHasOnTableFill;
    idr->flags |= flecs_observers_exist(o, id, EcsOnTableEmpty) * EcsIdHasOnTableEmpty;
    idr->flags |= flecs_observers_exist(o, id, EcsOnTableCreate) * EcsIdHasOnTableCreate;
    idr->flags |= flecs_observers_exist(o, id, EcsOnTableDelete) * EcsIdHasOnTableDelete;

    if (ecs_should_log_1()) {
        char *id_str = ecs_id_str(world, id);
        ecs_dbg_1("#[green]id#[normal] %s #[green]created", id_str);
        ecs_os_free(id_str);
    }

    /* Update counters */
    world->info.id_create_total ++;

    if (!is_wildcard) {
        world->info.id_count ++;

        if (idr->type_info) {
            world->info.component_id_count ++;
        } else {
            world->info.tag_id_count ++;
        }

        if (is_pair) {
            world->info.pair_id_count ++;
        }
    } else {
        world->info.wildcard_id_count ++;
    }

    return idr;
#ifdef FLECS_DEBUG
error:
    return NULL;
#endif
}

static
void flecs_id_record_assert_empty(
    ecs_id_record_t *idr)
{
    (void)idr;
    ecs_assert(flecs_table_cache_count(&idr->cache) == 0, 
        ECS_INTERNAL_ERROR, NULL);
    ecs_assert(flecs_table_cache_empty_count(&idr->cache) == 0, 
        ECS_INTERNAL_ERROR, NULL);
}

static
void flecs_id_record_free(
    ecs_world_t *world,
    ecs_id_record_t *idr)
{
    ecs_poly_assert(world, ecs_world_t);
    ecs_assert(idr != NULL, ECS_INTERNAL_ERROR, NULL);
    ecs_id_t id = idr->id;

    flecs_id_record_assert_empty(idr);

    /* Id is still in use by a filter, query, rule or observer */
    ecs_assert((world->flags & EcsWorldQuit) || (idr->keep_alive == 0), 
        ECS_ID_IN_USE, "cannot delete id that is queried for");

    if (ECS_IS_PAIR(id)) {
        ecs_entity_t rel = ECS_PAIR_FIRST(id);
        ecs_entity_t tgt = ECS_PAIR_SECOND(id);
        if (!ecs_id_is_wildcard(id)) {
            if (ECS_PAIR_FIRST(id) != EcsFlag) {
                /* If id is not a wildcard, remove it from the wildcard lists */
                flecs_remove_id_elem(idr, ecs_pair(rel, EcsWildcard));
                flecs_remove_id_elem(idr, ecs_pair(EcsWildcard, tgt));
            }
        } else {
            ecs_log_push_2();

            /* If id is a wildcard, it means that all id records that match the
             * wildcard are also empty, so release them */
            if (ECS_PAIR_FIRST(id) == EcsWildcard) {
                /* Iterate (*, Target) list */
                ecs_id_record_t *cur, *next = idr->second.next;
                while ((cur = next)) {
                    flecs_id_record_assert_empty(cur);
                    next = cur->second.next;
                    flecs_id_record_release(world, cur);
                }
            } else {
                /* Iterate (Relationship, *) list */
                ecs_assert(ECS_PAIR_SECOND(id) == EcsWildcard, 
                    ECS_INTERNAL_ERROR, NULL);
                ecs_id_record_t *cur, *next = idr->first.next;
                while ((cur = next)) {
                    flecs_id_record_assert_empty(cur);
                    next = cur->first.next;
                    flecs_id_record_release(world, cur);
                }
            }

            ecs_log_pop_2();
        }
    }

    /* Update counters */
    world->info.id_delete_total ++;

    if (!ecs_id_is_wildcard(id)) {
        world->info.id_count --;

        if (ECS_IS_PAIR(id)) {
            world->info.pair_id_count --;
        }

        if (idr->type_info) {
            world->info.component_id_count --;
        } else {
            world->info.tag_id_count --;
        }
    } else {
        world->info.wildcard_id_count --;
    }

    /* Unregister the id record from the world & free resources */
    ecs_table_cache_fini(&idr->cache);
    flecs_name_index_free(idr->name_index);
    ecs_vec_fini_t(&world->allocator, &idr->reachable.ids, ecs_reachable_elem_t);

    ecs_id_t hash = flecs_id_record_hash(id);
    if (hash >= FLECS_HI_ID_RECORD_ID) {
        ecs_map_remove(&world->id_index_hi, hash);
        flecs_bfree(&world->allocators.id_record, idr);
    } else {
        idr->id = 0; /* Tombstone */
    }

    if (ecs_should_log_1()) {
        char *id_str = ecs_id_str(world, id);
        ecs_dbg_1("#[green]id#[normal] %s #[red]deleted", id_str);
        ecs_os_free(id_str);
    }
}

ecs_id_record_t* flecs_id_record_ensure(
    ecs_world_t *world,
    ecs_id_t id)
{
    ecs_id_record_t *idr = flecs_id_record_get(world, id);
    if (!idr) {
        idr = flecs_id_record_new(world, id);
    }
    return idr;
}

ecs_id_record_t* flecs_id_record_get(
    const ecs_world_t *world,
    ecs_id_t id)
{
    ecs_poly_assert(world, ecs_world_t);
    if (id == ecs_pair(EcsIsA, EcsWildcard)) {
        return world->idr_isa_wildcard;
    } else if (id == ecs_pair(EcsChildOf, EcsWildcard)) {
        return world->idr_childof_wildcard;
    } else if (id == ecs_pair_t(EcsIdentifier, EcsName)) {
        return world->idr_identifier_name;
    }

    ecs_id_t hash = flecs_id_record_hash(id);
    ecs_id_record_t *idr = NULL;
    if (hash >= FLECS_HI_ID_RECORD_ID) {
        idr = ecs_map_get_deref(&world->id_index_hi, ecs_id_record_t, hash);
    } else {
        idr = &world->id_index_lo[hash];
        if (!idr->id) {
            idr = NULL;
        }
    }

    return idr;
}

ecs_id_record_t* flecs_query_id_record_get(
    const ecs_world_t *world,
    ecs_id_t id)
{
    ecs_id_record_t *idr = flecs_id_record_get(world, id);
    if (!idr) {
        ecs_entity_t first = ECS_PAIR_FIRST(id);
        if (ECS_IS_PAIR(id) && (first != EcsWildcard)) {
            idr = flecs_id_record_get(world, ecs_pair(EcsUnion, first));
        }
        return idr;
    }
    if (ECS_IS_PAIR(id) && 
        ECS_PAIR_SECOND(id) == EcsWildcard && 
        (idr->flags & EcsIdUnion)) 
    {
        idr = flecs_id_record_get(world, 
            ecs_pair(EcsUnion, ECS_PAIR_FIRST(id)));
    }

    return idr;
}

void flecs_id_record_claim(
    ecs_world_t *world,
    ecs_id_record_t *idr)
{
    (void)world;
    idr->refcount ++;
}

int32_t flecs_id_record_release(
    ecs_world_t *world,
    ecs_id_record_t *idr)
{
    int32_t rc = -- idr->refcount;
    ecs_assert(rc >= 0, ECS_INTERNAL_ERROR, NULL);

    if (!rc) {
        flecs_id_record_free(world, idr);
    }

    return rc;
}

void flecs_id_record_release_tables(
    ecs_world_t *world,
    ecs_id_record_t *idr)
{
    /* Cache should not contain tables that aren't empty */
    ecs_assert(flecs_table_cache_count(&idr->cache) == 0, 
        ECS_INTERNAL_ERROR, NULL);

    ecs_table_cache_iter_t it;
    if (flecs_table_cache_empty_iter(&idr->cache, &it)) {
        ecs_table_record_t *tr;
        while ((tr = flecs_table_cache_next(&it, ecs_table_record_t))) {
            /* Tables can hold claims on each other, so releasing a table can
             * cause the next element to get invalidated. Claim the next table
             * so that we can safely iterate. */
            ecs_table_t *next = NULL;
            if (it.next) {
                next = it.next->table;
                flecs_table_claim(world, next);
            }

            /* Release current table */
            flecs_table_release(world, tr->hdr.table);

            /* Check if the only thing keeping the next table alive is our
             * claim. If so, move to the next record before releasing */
            if (next) {
                if (next->_->refcount == 1) {
                    it.next = it.next->next;
                }

                flecs_table_release(world, next);
            }
        }
    }
}

bool flecs_id_record_set_type_info(
    ecs_world_t *world,
    ecs_id_record_t *idr,
    const ecs_type_info_t *ti)
{
    bool is_wildcard = ecs_id_is_wildcard(idr->id);
    if (!is_wildcard) {
        if (ti) {
            if (!idr->type_info) {
                world->info.tag_id_count --;
                world->info.component_id_count ++;
            }
        } else {
            if (idr->type_info) {
                world->info.tag_id_count ++;
                world->info.component_id_count --;
            }
        }
    }

    bool changed = idr->type_info != ti;
    idr->type_info = ti;

    return changed;
}

ecs_hashmap_t* flecs_id_record_name_index_ensure(
    ecs_world_t *world,
    ecs_id_record_t *idr)
{
    ecs_hashmap_t *map = idr->name_index;
    if (!map) {
        map = idr->name_index = flecs_name_index_new(world, &world->allocator);
    }

    return map;
}

ecs_hashmap_t* flecs_id_name_index_ensure(
    ecs_world_t *world,
    ecs_id_t id)
{
    ecs_poly_assert(world, ecs_world_t);

    ecs_id_record_t *idr = flecs_id_record_get(world, id);
    ecs_assert(idr != NULL, ECS_INTERNAL_ERROR, NULL);

    return flecs_id_record_name_index_ensure(world, idr);
}

ecs_hashmap_t* flecs_id_name_index_get(
    const ecs_world_t *world,
    ecs_id_t id)
{
    ecs_poly_assert(world, ecs_world_t);

    ecs_id_record_t *idr = flecs_id_record_get(world, id);
    if (!idr) {
        return NULL;
    }

    return idr->name_index;
}

ecs_table_record_t* flecs_table_record_get(
    const ecs_world_t *world,
    const ecs_table_t *table,
    ecs_id_t id)
{
    ecs_poly_assert(world, ecs_world_t);

    ecs_id_record_t* idr = flecs_id_record_get(world, id);
    if (!idr) {
        return NULL;
    }

    return (ecs_table_record_t*)ecs_table_cache_get(&idr->cache, table);
}

const ecs_table_record_t* flecs_id_record_get_table(
    const ecs_id_record_t *idr,
    const ecs_table_t *table)
{
    ecs_assert(idr != NULL, ECS_INTERNAL_ERROR, NULL);
    return (ecs_table_record_t*)ecs_table_cache_get(&idr->cache, table);
}

void flecs_init_id_records(
    ecs_world_t *world)
{
    /* Cache often used id records on world */
    world->idr_wildcard = flecs_id_record_ensure(world, EcsWildcard);
    world->idr_wildcard_wildcard = flecs_id_record_ensure(world, 
        ecs_pair(EcsWildcard, EcsWildcard));
    world->idr_any = flecs_id_record_ensure(world, EcsAny);
    world->idr_isa_wildcard = flecs_id_record_ensure(world, 
        ecs_pair(EcsIsA, EcsWildcard));
}

void flecs_fini_id_records(
    ecs_world_t *world)
{
    /* Loop & delete first element until there are no elements left. Id records
     * can recursively delete each other, this ensures we always have a
     * valid iterator. */
    while (ecs_map_count(&world->id_index_hi) > 0) {
        ecs_map_iter_t it = ecs_map_iter(&world->id_index_hi);
        ecs_map_next(&it);
        flecs_id_record_release(world, ecs_map_ptr(&it));
    }

    int32_t i;
    for (i = 0; i < FLECS_HI_ID_RECORD_ID; i ++) {
        ecs_id_record_t *idr = &world->id_index_lo[i];
        if (idr->id) {
            flecs_id_record_release(world, idr);
        }
    }

    ecs_assert(ecs_map_count(&world->id_index_hi) == 0, 
        ECS_INTERNAL_ERROR, NULL);

    ecs_map_fini(&world->id_index_hi);
    ecs_os_free(world->id_index_lo);
}

